1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-08 11:33:28 +00:00

Apply Prettier (#581)

This commit is contained in:
Oscar Hinton
2021-12-16 13:36:21 +01:00
committed by GitHub
parent 8b2dfc6cdc
commit 193434461d
589 changed files with 46650 additions and 41924 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,27 @@
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
import { AppIdService as AppIdServiceAbstraction } from '../abstractions/appId.service';
import { StorageService } from '../abstractions/storage.service';
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/appId.service";
import { StorageService } from "../abstractions/storage.service";
export class AppIdService implements AppIdServiceAbstraction {
constructor(private storageService: StorageService) {
constructor(private storageService: StorageService) {}
getAppId(): Promise<string> {
return this.makeAndGetAppId("appId");
}
getAnonymousAppId(): Promise<string> {
return this.makeAndGetAppId("anonymousAppId");
}
private async makeAndGetAppId(key: string) {
const existingId = await this.storageService.get<string>(key);
if (existingId != null) {
return existingId;
}
getAppId(): Promise<string> {
return this.makeAndGetAppId('appId');
}
getAnonymousAppId(): Promise<string> {
return this.makeAndGetAppId('anonymousAppId');
}
private async makeAndGetAppId(key: string) {
const existingId = await this.storageService.get<string>(key);
if (existingId != null) {
return existingId;
}
const guid = Utils.newGuid();
await this.storageService.save(key, guid);
return guid;
}
const guid = Utils.newGuid();
await this.storageService.save(key, guid);
return guid;
}
}

View File

@@ -1,43 +1,46 @@
import { ApiService } from '../abstractions/api.service';
import { AuditService as AuditServiceAbstraction } from '../abstractions/audit.service';
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { ApiService } from "../abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "../abstractions/audit.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { throttle } from '../misc/throttle';
import { Utils } from '../misc/utils';
import { throttle } from "../misc/throttle";
import { Utils } from "../misc/utils";
import { BreachAccountResponse } from '../models/response/breachAccountResponse';
import { ErrorResponse } from '../models/response/errorResponse';
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
import { ErrorResponse } from "../models/response/errorResponse";
const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/';
const PwnedPasswordsApi = "https://api.pwnedpasswords.com/range/";
export class AuditService implements AuditServiceAbstraction {
constructor(private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService) { }
constructor(
private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService
) {}
@throttle(100, () => 'passwordLeaked')
async passwordLeaked(password: string): Promise<number> {
const hashBytes = await this.cryptoFunctionService.hash(password, 'sha1');
const hash = Utils.fromBufferToHex(hashBytes).toUpperCase();
const hashStart = hash.substr(0, 5);
const hashEnding = hash.substr(5);
@throttle(100, () => "passwordLeaked")
async passwordLeaked(password: string): Promise<number> {
const hashBytes = await this.cryptoFunctionService.hash(password, "sha1");
const hash = Utils.fromBufferToHex(hashBytes).toUpperCase();
const hashStart = hash.substr(0, 5);
const hashEnding = hash.substr(5);
const response = await this.apiService.nativeFetch(new Request(PwnedPasswordsApi + hashStart));
const leakedHashes = await response.text();
const match = leakedHashes.split(/\r?\n/).find(v => {
return v.split(':')[0] === hashEnding;
});
const response = await this.apiService.nativeFetch(new Request(PwnedPasswordsApi + hashStart));
const leakedHashes = await response.text();
const match = leakedHashes.split(/\r?\n/).find((v) => {
return v.split(":")[0] === hashEnding;
});
return match != null ? parseInt(match.split(':')[1], 10) : 0;
}
async breachedAccounts(username: string): Promise<BreachAccountResponse[]> {
try {
return await this.apiService.getHibpBreach(username);
} catch (e) {
const error = e as ErrorResponse;
if (error.statusCode === 404) {
return [];
}
throw new Error();
}
return match != null ? parseInt(match.split(":")[1], 10) : 0;
}
async breachedAccounts(username: string): Promise<BreachAccountResponse[]> {
try {
return await this.apiService.getHibpBreach(username);
} catch (e) {
const error = e as ErrorResponse;
if (error.statusCode === 404) {
return [];
}
throw new Error();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,201 +1,215 @@
import { LogService } from '../abstractions/log.service';
import { LogService } from "../abstractions/log.service";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
import { EncArrayBuffer } from '../models/domain/encArrayBuffer';
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
const MAX_SINGLE_BLOB_UPLOAD_SIZE = 256 * 1024 * 1024; // 256 MiB
const MAX_BLOCKS_PER_BLOB = 50000;
export class AzureFileUploadService {
constructor(private logService: LogService) { }
constructor(private logService: LogService) {}
async upload(url: string, data: EncArrayBuffer, renewalCallback: () => Promise<string>) {
if (data.buffer.byteLength <= MAX_SINGLE_BLOB_UPLOAD_SIZE) {
return await this.azureUploadBlob(url, data);
} else {
return await this.azureUploadBlocks(url, data, renewalCallback);
}
async upload(url: string, data: EncArrayBuffer, renewalCallback: () => Promise<string>) {
if (data.buffer.byteLength <= MAX_SINGLE_BLOB_UPLOAD_SIZE) {
return await this.azureUploadBlob(url, data);
} else {
return await this.azureUploadBlocks(url, data, renewalCallback);
}
private async azureUploadBlob(url: string, data: EncArrayBuffer) {
const urlObject = Utils.getUrl(url);
const headers = new Headers({
'x-ms-date': new Date().toUTCString(),
'x-ms-version': urlObject.searchParams.get('sv'),
'Content-Length': data.buffer.byteLength.toString(),
'x-ms-blob-type': 'BlockBlob',
}
private async azureUploadBlob(url: string, data: EncArrayBuffer) {
const urlObject = Utils.getUrl(url);
const headers = new Headers({
"x-ms-date": new Date().toUTCString(),
"x-ms-version": urlObject.searchParams.get("sv"),
"Content-Length": data.buffer.byteLength.toString(),
"x-ms-blob-type": "BlockBlob",
});
const request = new Request(url, {
body: data.buffer,
cache: "no-store",
method: "PUT",
headers: headers,
});
const blobResponse = await fetch(request);
if (blobResponse.status !== 201) {
throw new Error(`Failed to create Azure blob: ${blobResponse.status}`);
}
}
private async azureUploadBlocks(
url: string,
data: EncArrayBuffer,
renewalCallback: () => Promise<string>
) {
const baseUrl = Utils.getUrl(url);
const blockSize = this.getMaxBlockSize(baseUrl.searchParams.get("sv"));
let blockIndex = 0;
const numBlocks = Math.ceil(data.buffer.byteLength / blockSize);
const blocksStaged: string[] = [];
if (numBlocks > MAX_BLOCKS_PER_BLOB) {
throw new Error(
`Cannot upload file, exceeds maximum size of ${blockSize * MAX_BLOCKS_PER_BLOB}`
);
}
try {
while (blockIndex < numBlocks) {
url = await this.renewUrlIfNecessary(url, renewalCallback);
const blockUrl = Utils.getUrl(url);
const blockId = this.encodedBlockId(blockIndex);
blockUrl.searchParams.append("comp", "block");
blockUrl.searchParams.append("blockid", blockId);
const start = blockIndex * blockSize;
const blockData = data.buffer.slice(start, start + blockSize);
const blockHeaders = new Headers({
"x-ms-date": new Date().toUTCString(),
"x-ms-version": blockUrl.searchParams.get("sv"),
"Content-Length": blockData.byteLength.toString(),
});
const request = new Request(url, {
body: data.buffer,
cache: 'no-store',
method: 'PUT',
headers: headers,
const blockRequest = new Request(blockUrl.toString(), {
body: blockData,
cache: "no-store",
method: "PUT",
headers: blockHeaders,
});
const blobResponse = await fetch(request);
const blockResponse = await fetch(blockRequest);
if (blobResponse.status !== 201) {
throw new Error(`Failed to create Azure blob: ${blobResponse.status}`);
}
}
private async azureUploadBlocks(url: string, data: EncArrayBuffer, renewalCallback: () => Promise<string>) {
const baseUrl = Utils.getUrl(url);
const blockSize = this.getMaxBlockSize(baseUrl.searchParams.get('sv'));
let blockIndex = 0;
const numBlocks = Math.ceil(data.buffer.byteLength / blockSize);
const blocksStaged: string[] = [];
if (numBlocks > MAX_BLOCKS_PER_BLOB) {
throw new Error(`Cannot upload file, exceeds maximum size of ${blockSize * MAX_BLOCKS_PER_BLOB}`);
if (blockResponse.status !== 201) {
const message = `Unsuccessful block PUT. Received status ${blockResponse.status}`;
this.logService.error(message + "\n" + (await blockResponse.json()));
throw new Error(message);
}
try {
while (blockIndex < numBlocks) {
url = await this.renewUrlIfNecessary(url, renewalCallback);
const blockUrl = Utils.getUrl(url);
const blockId = this.encodedBlockId(blockIndex);
blockUrl.searchParams.append('comp', 'block');
blockUrl.searchParams.append('blockid', blockId);
const start = blockIndex * blockSize;
const blockData = data.buffer.slice(start, start + blockSize);
const blockHeaders = new Headers({
'x-ms-date': new Date().toUTCString(),
'x-ms-version': blockUrl.searchParams.get('sv'),
'Content-Length': blockData.byteLength.toString(),
});
blocksStaged.push(blockId);
blockIndex++;
}
const blockRequest = new Request(blockUrl.toString(), {
body: blockData,
cache: 'no-store',
method: 'PUT',
headers: blockHeaders,
});
url = await this.renewUrlIfNecessary(url, renewalCallback);
const blockListUrl = Utils.getUrl(url);
const blockListXml = this.blockListXml(blocksStaged);
blockListUrl.searchParams.append("comp", "blocklist");
const headers = new Headers({
"x-ms-date": new Date().toUTCString(),
"x-ms-version": blockListUrl.searchParams.get("sv"),
"Content-Length": blockListXml.length.toString(),
});
const blockResponse = await fetch(blockRequest);
const request = new Request(blockListUrl.toString(), {
body: blockListXml,
cache: "no-store",
method: "PUT",
headers: headers,
});
if (blockResponse.status !== 201) {
const message = `Unsuccessful block PUT. Received status ${blockResponse.status}`;
this.logService.error(message + '\n' + await blockResponse.json());
throw new Error(message);
}
const response = await fetch(request);
blocksStaged.push(blockId);
blockIndex++;
}
if (response.status !== 201) {
const message = `Unsuccessful block list PUT. Received status ${response.status}`;
this.logService.error(message + "\n" + (await response.json()));
throw new Error(message);
}
} catch (e) {
throw e;
}
}
url = await this.renewUrlIfNecessary(url, renewalCallback);
const blockListUrl = Utils.getUrl(url);
const blockListXml = this.blockListXml(blocksStaged);
blockListUrl.searchParams.append('comp', 'blocklist');
const headers = new Headers({
'x-ms-date': new Date().toUTCString(),
'x-ms-version': blockListUrl.searchParams.get('sv'),
'Content-Length': blockListXml.length.toString(),
});
private async renewUrlIfNecessary(
url: string,
renewalCallback: () => Promise<string>
): Promise<string> {
const urlObject = Utils.getUrl(url);
const expiry = new Date(urlObject.searchParams.get("se") ?? "");
const request = new Request(blockListUrl.toString(), {
body: blockListXml,
cache: 'no-store',
method: 'PUT',
headers: headers,
});
const response = await fetch(request);
if (response.status !== 201) {
const message = `Unsuccessful block list PUT. Received status ${response.status}`;
this.logService.error(message + '\n' + await response.json());
throw new Error(message);
}
} catch (e) {
throw e;
}
if (isNaN(expiry.getTime())) {
expiry.setTime(Date.now() + 3600000);
}
private async renewUrlIfNecessary(url: string, renewalCallback: () => Promise<string>): Promise<string> {
const urlObject = Utils.getUrl(url);
const expiry = new Date(urlObject.searchParams.get('se') ?? '');
if (isNaN(expiry.getTime())) {
expiry.setTime(Date.now() + 3600000);
}
if (expiry.getTime() < Date.now() + 1000) {
return await renewalCallback();
}
return url;
if (expiry.getTime() < Date.now() + 1000) {
return await renewalCallback();
}
return url;
}
private encodedBlockId(blockIndex: number) {
// Encoded blockId max size is 64, so pre-encoding max size is 48
const utfBlockId = ('000000000000000000000000000000000000000000000000' + blockIndex.toString()).slice(-48);
return Utils.fromUtf8ToB64(utfBlockId);
}
private encodedBlockId(blockIndex: number) {
// Encoded blockId max size is 64, so pre-encoding max size is 48
const utfBlockId = (
"000000000000000000000000000000000000000000000000" + blockIndex.toString()
).slice(-48);
return Utils.fromUtf8ToB64(utfBlockId);
}
private blockListXml(blockIdList: string[]) {
let xml = '<?xml version="1.0" encoding="utf-8"?><BlockList>';
blockIdList.forEach(blockId => {
xml += `<Latest>${blockId}</Latest>`;
});
xml += '</BlockList>';
return xml;
}
private blockListXml(blockIdList: string[]) {
let xml = '<?xml version="1.0" encoding="utf-8"?><BlockList>';
blockIdList.forEach((blockId) => {
xml += `<Latest>${blockId}</Latest>`;
});
xml += "</BlockList>";
return xml;
}
private getMaxBlockSize(version: string) {
if (Version.compare(version, '2019-12-12') >= 0) {
return 4000 * 1024 * 1024; // 4000 MiB
} else if (Version.compare(version, '2016-05-31') >= 0) {
return 100 * 1024 * 1024; // 100 MiB
} else {
return 4 * 1024 * 1024; // 4 MiB
}
private getMaxBlockSize(version: string) {
if (Version.compare(version, "2019-12-12") >= 0) {
return 4000 * 1024 * 1024; // 4000 MiB
} else if (Version.compare(version, "2016-05-31") >= 0) {
return 100 * 1024 * 1024; // 100 MiB
} else {
return 4 * 1024 * 1024; // 4 MiB
}
}
}
class Version {
/**
* Compares two Azure Versions against each other
* @param a Version to compare
* @param b Version to compare
* @returns a number less than zero if b is newer than a, 0 if equal,
* and greater than zero if a is newer than b
*/
static compare(a: Required<Version> | string, b: Required<Version> | string) {
if (typeof (a) === 'string') {
a = new Version(a);
}
if (typeof (b) === 'string') {
b = new Version(b);
}
return a.year !== b.year ? a.year - b.year :
a.month !== b.month ? a.month - b.month :
a.day !== b.day ? a.day - b.day :
0;
/**
* Compares two Azure Versions against each other
* @param a Version to compare
* @param b Version to compare
* @returns a number less than zero if b is newer than a, 0 if equal,
* and greater than zero if a is newer than b
*/
static compare(a: Required<Version> | string, b: Required<Version> | string) {
if (typeof a === "string") {
a = new Version(a);
}
year = 0;
month = 0;
day = 0;
constructor(version: string) {
try {
const parts = version.split('-').map(v => Number.parseInt(v, 10));
this.year = parts[0];
this.month = parts[1];
this.day = parts[2];
} catch {
// Ignore error
}
if (typeof b === "string") {
b = new Version(b);
}
/**
* Compares two Azure Versions against each other
* @param compareTo Version to compare against
* @returns a number less than zero if compareTo is newer, 0 if equal,
* and greater than zero if this is greater than compareTo
*/
compare(compareTo: Required<Version> | string) {
return Version.compare(this, compareTo);
return a.year !== b.year
? a.year - b.year
: a.month !== b.month
? a.month - b.month
: a.day !== b.day
? a.day - b.day
: 0;
}
year = 0;
month = 0;
day = 0;
constructor(version: string) {
try {
const parts = version.split("-").map((v) => Number.parseInt(v, 10));
this.year = parts[0];
this.month = parts[1];
this.day = parts[2];
} catch {
// Ignore error
}
}
/**
* Compares two Azure Versions against each other
* @param compareTo Version to compare against
* @returns a number less than zero if compareTo is newer, 0 if equal,
* and greater than zero if this is greater than compareTo
*/
compare(compareTo: Required<Version> | string) {
return Version.compare(this, compareTo);
}
}

View File

@@ -1,29 +1,36 @@
import { ApiService } from '../abstractions/api.service';
import { ApiService } from "../abstractions/api.service";
import { EncArrayBuffer } from '../models/domain/encArrayBuffer';
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
export class BitwardenFileUploadService
{
constructor(private apiService: ApiService) { }
export class BitwardenFileUploadService {
constructor(private apiService: ApiService) {}
async upload(encryptedFileName: string, encryptedFileData: EncArrayBuffer, apiCall: (fd: FormData) => Promise<any>) {
const fd = new FormData();
try {
const blob = new Blob([encryptedFileData.buffer], { type: 'application/octet-stream' });
fd.append('data', blob, encryptedFileName);
} catch (e) {
if (Utils.isNode && !Utils.isBrowser) {
fd.append('data', Buffer.from(encryptedFileData.buffer) as any, {
filepath: encryptedFileName,
contentType: 'application/octet-stream',
} as any);
} else {
throw e;
}
}
await apiCall(fd);
async upload(
encryptedFileName: string,
encryptedFileData: EncArrayBuffer,
apiCall: (fd: FormData) => Promise<any>
) {
const fd = new FormData();
try {
const blob = new Blob([encryptedFileData.buffer], { type: "application/octet-stream" });
fd.append("data", blob, encryptedFileName);
} catch (e) {
if (Utils.isNode && !Utils.isBrowser) {
fd.append(
"data",
Buffer.from(encryptedFileData.buffer) as any,
{
filepath: encryptedFileName,
contentType: "application/octet-stream",
} as any
);
} else {
throw e;
}
}
await apiCall(fd);
}
}

View File

@@ -1,28 +1,28 @@
import { BroadcasterService as BroadcasterServiceAbstraction } from '../abstractions/broadcaster.service';
import { BroadcasterService as BroadcasterServiceAbstraction } from "../abstractions/broadcaster.service";
export class BroadcasterService implements BroadcasterServiceAbstraction {
subscribers: Map<string, (message: any) => any> = new Map<string, (message: any) => any>();
subscribers: Map<string, (message: any) => any> = new Map<string, (message: any) => any>();
send(message: any, id?: string) {
if (id != null) {
if (this.subscribers.has(id)) {
this.subscribers.get(id)(message);
}
return;
}
this.subscribers.forEach(value => {
value(message);
});
send(message: any, id?: string) {
if (id != null) {
if (this.subscribers.has(id)) {
this.subscribers.get(id)(message);
}
return;
}
subscribe(id: string, messageCallback: (message: any) => any) {
this.subscribers.set(id, messageCallback);
}
this.subscribers.forEach((value) => {
value(message);
});
}
unsubscribe(id: string) {
if (this.subscribers.has(id)) {
this.subscribers.delete(id);
}
subscribe(id: string, messageCallback: (message: any) => any) {
this.subscribers.set(id, messageCallback);
}
unsubscribe(id: string) {
if (this.subscribers.has(id)) {
this.subscribers.delete(id);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,157 +1,159 @@
import { CollectionData } from '../models/data/collectionData';
import { CollectionData } from "../models/data/collectionData";
import { Collection } from '../models/domain/collection';
import { TreeNode } from '../models/domain/treeNode';
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/treeNode";
import { CollectionView } from '../models/view/collectionView';
import { CollectionView } from "../models/view/collectionView";
import { CollectionService as CollectionServiceAbstraction } from '../abstractions/collection.service';
import { CryptoService } from '../abstractions/crypto.service';
import { I18nService } from '../abstractions/i18n.service';
import { StateService } from '../abstractions/state.service';
import { CollectionService as CollectionServiceAbstraction } from "../abstractions/collection.service";
import { CryptoService } from "../abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
import { StateService } from "../abstractions/state.service";
import { ServiceUtils } from '../misc/serviceUtils';
import { Utils } from '../misc/utils';
import { ServiceUtils } from "../misc/serviceUtils";
import { Utils } from "../misc/utils";
const NestingDelimiter = '/';
const NestingDelimiter = "/";
export class CollectionService implements CollectionServiceAbstraction {
constructor(private cryptoService: CryptoService, private i18nService: I18nService,
private stateService: StateService) {
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private stateService: StateService
) {}
async clearCache(userId?: string): Promise<void> {
await this.stateService.setDecryptedCollections(null, { userId: userId });
}
async encrypt(model: CollectionView): Promise<Collection> {
if (model.organizationId == null) {
throw new Error("Collection has no organization id.");
}
const key = await this.cryptoService.getOrgKey(model.organizationId);
if (key == null) {
throw new Error("No key for this collection's organization.");
}
const collection = new Collection();
collection.id = model.id;
collection.organizationId = model.organizationId;
collection.readOnly = model.readOnly;
collection.name = await this.cryptoService.encrypt(model.name, key);
return collection;
}
async decryptMany(collections: Collection[]): Promise<CollectionView[]> {
if (collections == null) {
return [];
}
const decCollections: CollectionView[] = [];
const promises: Promise<any>[] = [];
collections.forEach((collection) => {
promises.push(collection.decrypt().then((c) => decCollections.push(c)));
});
await Promise.all(promises);
return decCollections.sort(Utils.getSortFunction(this.i18nService, "name"));
}
async get(id: string): Promise<Collection> {
const collections = await this.stateService.getEncryptedCollections();
if (collections == null || !collections.hasOwnProperty(id)) {
return null;
}
async clearCache(userId?: string): Promise<void> {
await this.stateService.setDecryptedCollections(null, { userId: userId });
return new Collection(collections[id]);
}
async getAll(): Promise<Collection[]> {
const collections = await this.stateService.getEncryptedCollections();
const response: Collection[] = [];
for (const id in collections) {
if (collections.hasOwnProperty(id)) {
response.push(new Collection(collections[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<CollectionView[]> {
let decryptedCollections = await this.stateService.getDecryptedCollections();
if (decryptedCollections != null) {
return decryptedCollections;
}
async encrypt(model: CollectionView): Promise<Collection> {
if (model.organizationId == null) {
throw new Error('Collection has no organization id.');
}
const key = await this.cryptoService.getOrgKey(model.organizationId);
if (key == null) {
throw new Error('No key for this collection\'s organization.');
}
const collection = new Collection();
collection.id = model.id;
collection.organizationId = model.organizationId;
collection.readOnly = model.readOnly;
collection.name = await this.cryptoService.encrypt(model.name, key);
return collection;
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error("No key.");
}
async decryptMany(collections: Collection[]): Promise<CollectionView[]> {
if (collections == null) {
return [];
}
const decCollections: CollectionView[] = [];
const promises: Promise<any>[] = [];
collections.forEach(collection => {
promises.push(collection.decrypt().then(c => decCollections.push(c)));
});
await Promise.all(promises);
return decCollections.sort(Utils.getSortFunction(this.i18nService, 'name'));
const collections = await this.getAll();
decryptedCollections = await this.decryptMany(collections);
await this.stateService.setDecryptedCollections(decryptedCollections);
return decryptedCollections;
}
async getAllNested(collections: CollectionView[] = null): Promise<TreeNode<CollectionView>[]> {
if (collections == null) {
collections = await this.getAllDecrypted();
}
const nodes: TreeNode<CollectionView>[] = [];
collections.forEach((c) => {
const collectionCopy = new CollectionView();
collectionCopy.id = c.id;
collectionCopy.organizationId = c.organizationId;
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
});
return nodes;
}
async getNested(id: string): Promise<TreeNode<CollectionView>> {
const collections = await this.getAllNested();
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionView>;
}
async upsert(collection: CollectionData | CollectionData[]): Promise<any> {
let collections = await this.stateService.getEncryptedCollections();
if (collections == null) {
collections = {};
}
async get(id: string): Promise<Collection> {
const collections = await this.stateService.getEncryptedCollections();
if (collections == null || !collections.hasOwnProperty(id)) {
return null;
}
return new Collection(collections[id]);
if (collection instanceof CollectionData) {
const c = collection as CollectionData;
collections[c.id] = c;
} else {
(collection as CollectionData[]).forEach((c) => {
collections[c.id] = c;
});
}
async getAll(): Promise<Collection[]> {
const collections = await this.stateService.getEncryptedCollections();
const response: Collection[] = [];
for (const id in collections) {
if (collections.hasOwnProperty(id)) {
response.push(new Collection(collections[id]));
}
}
return response;
await this.replace(collections);
}
async replace(collections: { [id: string]: CollectionData }): Promise<any> {
await this.clearCache();
await this.stateService.setEncryptedCollections(collections);
}
async clear(userId?: string): Promise<any> {
await this.clearCache(userId);
await this.stateService.setEncryptedCollections(null, { userId: userId });
}
async delete(id: string | string[]): Promise<any> {
const collections = await this.stateService.getEncryptedCollections();
if (collections == null) {
return;
}
async getAllDecrypted(): Promise<CollectionView[]> {
let decryptedCollections = await this.stateService.getDecryptedCollections();
if (decryptedCollections != null) {
return decryptedCollections;
}
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error('No key.');
}
const collections = await this.getAll();
decryptedCollections = await this.decryptMany(collections);
await this.stateService.setDecryptedCollections(decryptedCollections);
return decryptedCollections;
if (typeof id === "string") {
delete collections[id];
} else {
(id as string[]).forEach((i) => {
delete collections[i];
});
}
async getAllNested(collections: CollectionView[] = null): Promise<TreeNode<CollectionView>[]> {
if (collections == null) {
collections = await this.getAllDecrypted();
}
const nodes: TreeNode<CollectionView>[] = [];
collections.forEach(c => {
const collectionCopy = new CollectionView();
collectionCopy.id = c.id;
collectionCopy.organizationId = c.organizationId;
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
});
return nodes;
}
async getNested(id: string): Promise<TreeNode<CollectionView>> {
const collections = await this.getAllNested();
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionView>;
}
async upsert(collection: CollectionData | CollectionData[]): Promise<any> {
let collections = await this.stateService.getEncryptedCollections();
if (collections == null) {
collections = {};
}
if (collection instanceof CollectionData) {
const c = collection as CollectionData;
collections[c.id] = c;
} else {
(collection as CollectionData[]).forEach(c => {
collections[c.id] = c;
});
}
await this.replace(collections);
}
async replace(collections: { [id: string]: CollectionData; }): Promise<any> {
await this.clearCache();
await this.stateService.setEncryptedCollections(collections);
}
async clear(userId?: string): Promise<any> {
await this.clearCache(userId);
await this.stateService.setEncryptedCollections(null, { userId: userId });
}
async delete(id: string | string[]): Promise<any> {
const collections = await this.stateService.getEncryptedCollections();
if (collections == null) {
return;
}
if (typeof id === 'string') {
delete collections[id];
} else {
(id as string[]).forEach(i => {
delete collections[i];
});
}
await this.replace(collections);
}
await this.replace(collections);
}
}

View File

@@ -1,70 +1,73 @@
import { LogLevelType } from '../enums/logLevelType';
import { LogLevelType } from "../enums/logLevelType";
import { LogService as LogServiceAbstraction } from '../abstractions/log.service';
import { LogService as LogServiceAbstraction } from "../abstractions/log.service";
import * as hrtime from 'browser-hrtime';
import * as hrtime from "browser-hrtime";
export class ConsoleLogService implements LogServiceAbstraction {
protected timersMap: Map<string, [number, number]> = new Map();
protected timersMap: Map<string, [number, number]> = new Map();
constructor(protected isDev: boolean, protected filter: (level: LogLevelType) => boolean = null) { }
constructor(
protected isDev: boolean,
protected filter: (level: LogLevelType) => boolean = null
) {}
debug(message: string) {
if (!this.isDev) {
return;
}
this.write(LogLevelType.Debug, message);
debug(message: string) {
if (!this.isDev) {
return;
}
this.write(LogLevelType.Debug, message);
}
info(message: string) {
this.write(LogLevelType.Info, message);
}
warning(message: string) {
this.write(LogLevelType.Warning, message);
}
error(message: string) {
this.write(LogLevelType.Error, message);
}
write(level: LogLevelType, message: string) {
if (this.filter != null && this.filter(level)) {
return;
}
info(message: string) {
this.write(LogLevelType.Info, message);
switch (level) {
case LogLevelType.Debug:
// tslint:disable-next-line
console.log(message);
break;
case LogLevelType.Info:
// tslint:disable-next-line
console.log(message);
break;
case LogLevelType.Warning:
// tslint:disable-next-line
console.warn(message);
break;
case LogLevelType.Error:
// tslint:disable-next-line
console.error(message);
break;
default:
break;
}
}
warning(message: string) {
this.write(LogLevelType.Warning, message);
time(label: string = "default") {
if (!this.timersMap.has(label)) {
this.timersMap.set(label, hrtime());
}
}
error(message: string) {
this.write(LogLevelType.Error, message);
}
write(level: LogLevelType, message: string) {
if (this.filter != null && this.filter(level)) {
return;
}
switch (level) {
case LogLevelType.Debug:
// tslint:disable-next-line
console.log(message);
break;
case LogLevelType.Info:
// tslint:disable-next-line
console.log(message);
break;
case LogLevelType.Warning:
// tslint:disable-next-line
console.warn(message);
break;
case LogLevelType.Error:
// tslint:disable-next-line
console.error(message);
break;
default:
break;
}
}
time(label: string = 'default') {
if (!this.timersMap.has(label)) {
this.timersMap.set(label, hrtime());
}
}
timeEnd(label: string = 'default'): [number, number] {
const elapsed = hrtime(this.timersMap.get(label));
this.timersMap.delete(label);
this.write(LogLevelType.Info, `${label}: ${elapsed[0] * 1000 + elapsed[1] / 10e6}ms`);
return elapsed;
}
timeEnd(label: string = "default"): [number, number] {
const elapsed = hrtime(this.timersMap.get(label));
this.timersMap.delete(label);
this.write(LogLevelType.Info, `${label}: ${elapsed[0] * 1000 + elapsed[1] / 10e6}ms`);
return elapsed;
}
}

View File

@@ -1,21 +1,20 @@
import { CryptoService } from '../abstractions/crypto.service';
import { CryptoService } from "../abstractions/crypto.service";
export class ContainerService {
constructor(private cryptoService: CryptoService) {
}
constructor(private cryptoService: CryptoService) {}
// deprecated, use attachToGlobal instead
attachToWindow(win: any) {
this.attachToGlobal(win);
}
// deprecated, use attachToGlobal instead
attachToWindow(win: any) {
this.attachToGlobal(win);
}
attachToGlobal(global: any) {
if (!global.bitwardenContainerService) {
global.bitwardenContainerService = this;
}
attachToGlobal(global: any) {
if (!global.bitwardenContainerService) {
global.bitwardenContainerService = this;
}
}
getCryptoService(): CryptoService {
return this.cryptoService;
}
getCryptoService(): CryptoService {
return this.cryptoService;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,200 +1,202 @@
import { Observable, Subject } from 'rxjs';
import { Observable, Subject } from "rxjs";
import { EnvironmentUrls } from '../models/domain/environmentUrls';
import { EnvironmentUrls } from "../models/domain/environmentUrls";
import { EnvironmentService as EnvironmentServiceAbstraction, Urls } from '../abstractions/environment.service';
import { StateService } from '../abstractions/state.service';
import {
EnvironmentService as EnvironmentServiceAbstraction,
Urls,
} from "../abstractions/environment.service";
import { StateService } from "../abstractions/state.service";
export class EnvironmentService implements EnvironmentServiceAbstraction {
private readonly urlsSubject = new Subject<Urls>();
urls: Observable<Urls> = this.urlsSubject; // tslint:disable-line
private readonly urlsSubject = new Subject<Urls>();
urls: Observable<Urls> = this.urlsSubject; // tslint:disable-line
private baseUrl: string;
private webVaultUrl: string;
private apiUrl: string;
private identityUrl: string;
private iconsUrl: string;
private notificationsUrl: string;
private eventsUrl: string;
private keyConnectorUrl: string;
private baseUrl: string;
private webVaultUrl: string;
private apiUrl: string;
private identityUrl: string;
private iconsUrl: string;
private notificationsUrl: string;
private eventsUrl: string;
private keyConnectorUrl: string;
constructor(private stateService: StateService) {}
constructor(private stateService: StateService) {}
hasBaseUrl() {
return this.baseUrl != null;
}
hasBaseUrl() {
return this.baseUrl != null;
getNotificationsUrl() {
if (this.notificationsUrl != null) {
return this.notificationsUrl;
}
getNotificationsUrl() {
if (this.notificationsUrl != null) {
return this.notificationsUrl;
}
if (this.baseUrl != null) {
return this.baseUrl + '/notifications';
}
return 'https://notifications.bitwarden.com';
if (this.baseUrl != null) {
return this.baseUrl + "/notifications";
}
getWebVaultUrl() {
if (this.webVaultUrl != null) {
return this.webVaultUrl;
}
return "https://notifications.bitwarden.com";
}
if (this.baseUrl) {
return this.baseUrl;
}
return 'https://vault.bitwarden.com';
getWebVaultUrl() {
if (this.webVaultUrl != null) {
return this.webVaultUrl;
}
getSendUrl() {
return this.getWebVaultUrl() === 'https://vault.bitwarden.com'
? 'https://send.bitwarden.com/#'
: this.getWebVaultUrl() + '/#/send/';
if (this.baseUrl) {
return this.baseUrl;
}
return "https://vault.bitwarden.com";
}
getSendUrl() {
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
? "https://send.bitwarden.com/#"
: this.getWebVaultUrl() + "/#/send/";
}
getIconsUrl() {
if (this.iconsUrl != null) {
return this.iconsUrl;
}
getIconsUrl() {
if (this.iconsUrl != null) {
return this.iconsUrl;
}
if (this.baseUrl) {
return this.baseUrl + '/icons';
}
return 'https://icons.bitwarden.net';
if (this.baseUrl) {
return this.baseUrl + "/icons";
}
getApiUrl() {
if (this.apiUrl != null) {
return this.apiUrl;
}
return "https://icons.bitwarden.net";
}
if (this.baseUrl) {
return this.baseUrl + '/api';
}
return 'https://api.bitwarden.com';
getApiUrl() {
if (this.apiUrl != null) {
return this.apiUrl;
}
getIdentityUrl() {
if (this.identityUrl != null) {
return this.identityUrl;
}
if (this.baseUrl) {
return this.baseUrl + '/identity';
}
return 'https://identity.bitwarden.com';
if (this.baseUrl) {
return this.baseUrl + "/api";
}
getEventsUrl() {
if (this.eventsUrl != null) {
return this.eventsUrl;
}
return "https://api.bitwarden.com";
}
if (this.baseUrl) {
return this.baseUrl + '/events';
}
return 'https://events.bitwarden.com';
getIdentityUrl() {
if (this.identityUrl != null) {
return this.identityUrl;
}
getKeyConnectorUrl() {
return this.keyConnectorUrl;
if (this.baseUrl) {
return this.baseUrl + "/identity";
}
async setUrlsFromStorage(): Promise<void> {
const urlsObj: any = await this.stateService.getEnvironmentUrls();
const urls = urlsObj || {
base: null,
api: null,
identity: null,
icons: null,
notifications: null,
events: null,
webVault: null,
keyConnector: null,
};
return "https://identity.bitwarden.com";
}
const envUrls = new EnvironmentUrls();
if (urls.base) {
this.baseUrl = envUrls.base = urls.base;
return;
}
this.webVaultUrl = urls.webVault;
this.apiUrl = envUrls.api = urls.api;
this.identityUrl = envUrls.identity = urls.identity;
this.iconsUrl = urls.icons;
this.notificationsUrl = urls.notifications;
this.eventsUrl = envUrls.events = urls.events;
this.keyConnectorUrl = urls.keyConnector;
getEventsUrl() {
if (this.eventsUrl != null) {
return this.eventsUrl;
}
async setUrls(urls: Urls, saveSettings: boolean = true): 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);
urls.notifications = this.formatUrl(urls.notifications);
urls.events = this.formatUrl(urls.events);
urls.keyConnector = this.formatUrl(urls.keyConnector);
if (saveSettings) {
await this.stateService.setEnvironmentUrls({
base: urls.base,
api: urls.api,
identity: urls.identity,
webVault: urls.webVault,
icons: urls.icons,
notifications: urls.notifications,
events: urls.events,
keyConnector: urls.keyConnector,
});
}
this.baseUrl = urls.base;
this.webVaultUrl = urls.webVault;
this.apiUrl = urls.api;
this.identityUrl = urls.identity;
this.iconsUrl = urls.icons;
this.notificationsUrl = urls.notifications;
this.eventsUrl = urls.events;
this.keyConnectorUrl = urls.keyConnector;
this.urlsSubject.next(urls);
return urls;
if (this.baseUrl) {
return this.baseUrl + "/events";
}
getUrls() {
return {
base: this.baseUrl,
webVault: this.webVaultUrl,
api: this.apiUrl,
identity: this.identityUrl,
icons: this.iconsUrl,
notifications: this.notificationsUrl,
events: this.eventsUrl,
keyConnector: this.keyConnectorUrl,
};
return "https://events.bitwarden.com";
}
getKeyConnectorUrl() {
return this.keyConnectorUrl;
}
async setUrlsFromStorage(): Promise<void> {
const urlsObj: any = await this.stateService.getEnvironmentUrls();
const urls = urlsObj || {
base: null,
api: null,
identity: null,
icons: null,
notifications: null,
events: null,
webVault: null,
keyConnector: null,
};
const envUrls = new EnvironmentUrls();
if (urls.base) {
this.baseUrl = envUrls.base = urls.base;
return;
}
private formatUrl(url: string): string {
if (url == null || url === '') {
return null;
}
this.webVaultUrl = urls.webVault;
this.apiUrl = envUrls.api = urls.api;
this.identityUrl = envUrls.identity = urls.identity;
this.iconsUrl = urls.icons;
this.notificationsUrl = urls.notifications;
this.eventsUrl = envUrls.events = urls.events;
this.keyConnectorUrl = urls.keyConnector;
}
url = url.replace(/\/+$/g, '');
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
async setUrls(urls: Urls, saveSettings: boolean = true): 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);
urls.notifications = this.formatUrl(urls.notifications);
urls.events = this.formatUrl(urls.events);
urls.keyConnector = this.formatUrl(urls.keyConnector);
return url.trim();
if (saveSettings) {
await this.stateService.setEnvironmentUrls({
base: urls.base,
api: urls.api,
identity: urls.identity,
webVault: urls.webVault,
icons: urls.icons,
notifications: urls.notifications,
events: urls.events,
keyConnector: urls.keyConnector,
});
}
this.baseUrl = urls.base;
this.webVaultUrl = urls.webVault;
this.apiUrl = urls.api;
this.identityUrl = urls.identity;
this.iconsUrl = urls.icons;
this.notificationsUrl = urls.notifications;
this.eventsUrl = urls.events;
this.keyConnectorUrl = urls.keyConnector;
this.urlsSubject.next(urls);
return urls;
}
getUrls() {
return {
base: this.baseUrl,
webVault: this.webVaultUrl,
api: this.apiUrl,
identity: this.identityUrl,
icons: this.iconsUrl,
notifications: this.notificationsUrl,
events: this.eventsUrl,
keyConnector: this.keyConnectorUrl,
};
}
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.trim();
}
}

View File

@@ -1,94 +1,102 @@
import { EventType } from '../enums/eventType';
import { EventType } from "../enums/eventType";
import { EventData } from '../models/data/eventData';
import { EventData } from "../models/data/eventData";
import { EventRequest } from '../models/request/eventRequest';
import { EventRequest } from "../models/request/eventRequest";
import { ApiService } from '../abstractions/api.service';
import { CipherService } from '../abstractions/cipher.service';
import { EventService as EventServiceAbstraction } from '../abstractions/event.service';
import { LogService } from '../abstractions/log.service';
import { OrganizationService } from '../abstractions/organization.service';
import { StateService } from '../abstractions/state.service';
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { EventService as EventServiceAbstraction } from "../abstractions/event.service";
import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization.service";
import { StateService } from "../abstractions/state.service";
export class EventService implements EventServiceAbstraction {
private inited = false;
private inited = false;
constructor(private apiService: ApiService, private cipherService: CipherService,
private stateService: StateService, private logService: LogService,
private organizationService: OrganizationService) { }
constructor(
private apiService: ApiService,
private cipherService: CipherService,
private stateService: StateService,
private logService: LogService,
private organizationService: OrganizationService
) {}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
this.inited = true;
if (checkOnInterval) {
this.uploadEvents();
setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds
}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
async collect(eventType: EventType, cipherId: string = null, uploadImmediately = false): Promise<any> {
const authed = await this.stateService.getIsAuthenticated();
if (!authed) {
return;
}
const organizations = await this.organizationService.getAll();
if (organizations == null) {
return;
}
const orgIds = new Set<string>(organizations.filter(o => o.useEvents).map(o => o.id));
if (orgIds.size === 0) {
return;
}
if (cipherId != null) {
const cipher = await this.cipherService.get(cipherId);
if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) {
return;
}
}
let eventCollection = await this.stateService.getEventCollection();
if (eventCollection == null) {
eventCollection = [];
}
const event = new EventData();
event.type = eventType;
event.cipherId = cipherId;
event.date = new Date().toISOString();
eventCollection.push(event);
await this.stateService.setEventCollection(eventCollection);
if (uploadImmediately) {
await this.uploadEvents();
}
this.inited = true;
if (checkOnInterval) {
this.uploadEvents();
setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds
}
}
async uploadEvents(userId?: string): Promise<any> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
if (eventCollection == null || eventCollection.length === 0) {
return;
}
const request = eventCollection.map(e => {
const req = new EventRequest();
req.type = e.type;
req.cipherId = e.cipherId;
req.date = e.date;
return req;
});
try {
await this.apiService.postEventsCollect(request);
this.clearEvents(userId);
} catch (e) {
this.logService.error(e);
}
async collect(
eventType: EventType,
cipherId: string = null,
uploadImmediately = false
): Promise<any> {
const authed = await this.stateService.getIsAuthenticated();
if (!authed) {
return;
}
const organizations = await this.organizationService.getAll();
if (organizations == null) {
return;
}
const orgIds = new Set<string>(organizations.filter((o) => o.useEvents).map((o) => o.id));
if (orgIds.size === 0) {
return;
}
if (cipherId != null) {
const cipher = await this.cipherService.get(cipherId);
if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) {
return;
}
}
let eventCollection = await this.stateService.getEventCollection();
if (eventCollection == null) {
eventCollection = [];
}
const event = new EventData();
event.type = eventType;
event.cipherId = cipherId;
event.date = new Date().toISOString();
eventCollection.push(event);
await this.stateService.setEventCollection(eventCollection);
if (uploadImmediately) {
await this.uploadEvents();
}
}
async clearEvents(userId?: string): Promise<any> {
await this.stateService.setEventCollection(null, { userId: userId });
async uploadEvents(userId?: string): Promise<any> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
if (eventCollection == null || eventCollection.length === 0) {
return;
}
const request = eventCollection.map((e) => {
const req = new EventRequest();
req.type = e.type;
req.cipherId = e.cipherId;
req.date = e.date;
return req;
});
try {
await this.apiService.postEventsCollect(request);
this.clearEvents(userId);
} catch (e) {
this.logService.error(e);
}
}
async clearEvents(userId?: string): Promise<any> {
await this.stateService.setEventCollection(null, { userId: userId });
}
}

View File

@@ -1,369 +1,410 @@
import * as papa from 'papaparse';
import * as papa from "papaparse";
import { CipherType } from '../enums/cipherType';
import { CipherType } from "../enums/cipherType";
import { ApiService } from '../abstractions/api.service';
import { CipherService } from '../abstractions/cipher.service';
import { CryptoService } from '../abstractions/crypto.service';
import { ExportService as ExportServiceAbstraction } from '../abstractions/export.service';
import { FolderService } from '../abstractions/folder.service';
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { CryptoService } from "../abstractions/crypto.service";
import { ExportService as ExportServiceAbstraction } from "../abstractions/export.service";
import { FolderService } from "../abstractions/folder.service";
import { CipherView } from '../models/view/cipherView';
import { CollectionView } from '../models/view/collectionView';
import { FolderView } from '../models/view/folderView';
import { CipherView } from "../models/view/cipherView";
import { CollectionView } from "../models/view/collectionView";
import { FolderView } from "../models/view/folderView";
import { Cipher } from '../models/domain/cipher';
import { Collection } from '../models/domain/collection';
import { Folder } from '../models/domain/folder';
import { Cipher } from "../models/domain/cipher";
import { Collection } from "../models/domain/collection";
import { Folder } from "../models/domain/folder";
import { CipherData } from '../models/data/cipherData';
import { CollectionData } from '../models/data/collectionData';
import { CollectionDetailsResponse } from '../models/response/collectionResponse';
import { CipherData } from "../models/data/cipherData";
import { CollectionData } from "../models/data/collectionData";
import { CollectionDetailsResponse } from "../models/response/collectionResponse";
import { CipherWithIds as CipherExport } from '../models/export/cipherWithIds';
import { CollectionWithId as CollectionExport } from '../models/export/collectionWithId';
import { Event } from '../models/export/event';
import { FolderWithId as FolderExport } from '../models/export/folderWithId';
import { EventView } from '../models/view/eventView';
import { CipherWithIds as CipherExport } from "../models/export/cipherWithIds";
import { CollectionWithId as CollectionExport } from "../models/export/collectionWithId";
import { Event } from "../models/export/event";
import { FolderWithId as FolderExport } from "../models/export/folderWithId";
import { EventView } from "../models/view/eventView";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
export class ExportService implements ExportServiceAbstraction {
constructor(private folderService: FolderService, private cipherService: CipherService,
private apiService: ApiService, private cryptoService: CryptoService) { }
constructor(
private folderService: FolderService,
private cipherService: CipherService,
private apiService: ApiService,
private cryptoService: CryptoService
) {}
async getExport(format: 'csv' | 'json' | 'encrypted_json' = 'csv'): Promise<string> {
if (format === 'encrypted_json') {
return this.getEncryptedExport();
async getExport(format: "csv" | "json" | "encrypted_json" = "csv"): Promise<string> {
if (format === "encrypted_json") {
return this.getEncryptedExport();
} else {
return this.getDecryptedExport(format);
}
}
async getOrganizationExport(
organizationId: string,
format: "csv" | "json" | "encrypted_json" = "csv"
): Promise<string> {
if (format === "encrypted_json") {
return this.getOrganizationEncryptedExport(organizationId);
} else {
return this.getOrganizationDecryptedExport(organizationId, format);
}
}
async getEventExport(events: EventView[]): Promise<string> {
return papa.unparse(events.map((e) => new Event(e)));
}
getFileName(prefix: string = null, extension: string = "csv"): string {
const now = new Date();
const dateString =
now.getFullYear() +
"" +
this.padNumber(now.getMonth() + 1, 2) +
"" +
this.padNumber(now.getDate(), 2) +
this.padNumber(now.getHours(), 2) +
"" +
this.padNumber(now.getMinutes(), 2) +
this.padNumber(now.getSeconds(), 2);
return "bitwarden" + (prefix ? "_" + prefix : "") + "_export_" + dateString + "." + extension;
}
private async getDecryptedExport(format: "json" | "csv"): Promise<string> {
let decFolders: FolderView[] = [];
let decCiphers: CipherView[] = [];
const promises = [];
promises.push(
this.folderService.getAllDecrypted().then((folders) => {
decFolders = folders;
})
);
promises.push(
this.cipherService.getAllDecrypted().then((ciphers) => {
decCiphers = ciphers.filter((f) => f.deletedDate == null);
})
);
await Promise.all(promises);
if (format === "csv") {
const foldersMap = new Map<string, FolderView>();
decFolders.forEach((f) => {
if (f.id != null) {
foldersMap.set(f.id, f);
}
});
const exportCiphers: any[] = [];
decCiphers.forEach((c) => {
// only export logins and secure notes
if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) {
return;
}
if (c.organizationId != null) {
return;
}
const cipher: any = {};
cipher.folder =
c.folderId != null && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null;
cipher.favorite = c.favorite ? 1 : null;
this.buildCommonCipher(cipher, c);
exportCiphers.push(cipher);
});
return papa.unparse(exportCiphers);
} else {
const jsonDoc: any = {
encrypted: false,
folders: [],
items: [],
};
decFolders.forEach((f) => {
if (f.id == null) {
return;
}
const folder = new FolderExport();
folder.build(f);
jsonDoc.folders.push(folder);
});
decCiphers.forEach((c) => {
if (c.organizationId != null) {
return;
}
const cipher = new CipherExport();
cipher.build(c);
cipher.collectionIds = null;
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, " ");
}
}
private async getEncryptedExport(): Promise<string> {
let folders: Folder[] = [];
let ciphers: Cipher[] = [];
const promises = [];
promises.push(
this.folderService.getAll().then((f) => {
folders = f;
})
);
promises.push(
this.cipherService.getAll().then((c) => {
ciphers = c.filter((f) => f.deletedDate == null);
})
);
await Promise.all(promises);
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid());
const jsonDoc: any = {
encrypted: true,
encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString,
folders: [],
items: [],
};
folders.forEach((f) => {
if (f.id == null) {
return;
}
const folder = new FolderExport();
folder.build(f);
jsonDoc.folders.push(folder);
});
ciphers.forEach((c) => {
if (c.organizationId != null) {
return;
}
const cipher = new CipherExport();
cipher.build(c);
cipher.collectionIds = null;
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, " ");
}
private async getOrganizationDecryptedExport(
organizationId: string,
format: "json" | "csv"
): Promise<string> {
const decCollections: CollectionView[] = [];
const decCiphers: CipherView[] = [];
const promises = [];
promises.push(
this.apiService.getCollections(organizationId).then((collections) => {
const collectionPromises: any = [];
if (collections != null && collections.data != null && collections.data.length > 0) {
collections.data.forEach((c) => {
const collection = new Collection(new CollectionData(c as CollectionDetailsResponse));
collectionPromises.push(
collection.decrypt().then((decCol) => {
decCollections.push(decCol);
})
);
});
}
return Promise.all(collectionPromises);
})
);
promises.push(
this.apiService.getCiphersOrganization(organizationId).then((ciphers) => {
const cipherPromises: any = [];
if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) {
ciphers.data
.filter((c) => c.deletedDate === null)
.forEach((c) => {
const cipher = new Cipher(new CipherData(c));
cipherPromises.push(
cipher.decrypt().then((decCipher) => {
decCiphers.push(decCipher);
})
);
});
}
return Promise.all(cipherPromises);
})
);
await Promise.all(promises);
if (format === "csv") {
const collectionsMap = new Map<string, CollectionView>();
decCollections.forEach((c) => {
collectionsMap.set(c.id, c);
});
const exportCiphers: any[] = [];
decCiphers.forEach((c) => {
// only export logins and secure notes
if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) {
return;
}
const cipher: any = {};
cipher.collections = [];
if (c.collectionIds != null) {
cipher.collections = c.collectionIds
.filter((id) => collectionsMap.has(id))
.map((id) => collectionsMap.get(id).name);
}
this.buildCommonCipher(cipher, c);
exportCiphers.push(cipher);
});
return papa.unparse(exportCiphers);
} else {
const jsonDoc: any = {
encrypted: false,
collections: [],
items: [],
};
decCollections.forEach((c) => {
const collection = new CollectionExport();
collection.build(c);
jsonDoc.collections.push(collection);
});
decCiphers.forEach((c) => {
const cipher = new CipherExport();
cipher.build(c);
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, " ");
}
}
private async getOrganizationEncryptedExport(organizationId: string): Promise<string> {
const collections: Collection[] = [];
const ciphers: Cipher[] = [];
const promises = [];
promises.push(
this.apiService.getCollections(organizationId).then((c) => {
const collectionPromises: any = [];
if (c != null && c.data != null && c.data.length > 0) {
c.data.forEach((r) => {
const collection = new Collection(new CollectionData(r as CollectionDetailsResponse));
collections.push(collection);
});
}
return Promise.all(collectionPromises);
})
);
promises.push(
this.apiService.getCiphersOrganization(organizationId).then((c) => {
const cipherPromises: any = [];
if (c != null && c.data != null && c.data.length > 0) {
c.data
.filter((item) => item.deletedDate === null)
.forEach((item) => {
const cipher = new Cipher(new CipherData(item));
ciphers.push(cipher);
});
}
return Promise.all(cipherPromises);
})
);
await Promise.all(promises);
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), orgKey);
const jsonDoc: any = {
encrypted: true,
encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString,
collections: [],
items: [],
};
collections.forEach((c) => {
const collection = new CollectionExport();
collection.build(c);
jsonDoc.collections.push(collection);
});
ciphers.forEach((c) => {
const cipher = new CipherExport();
cipher.build(c);
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, " ");
}
private padNumber(num: number, width: number, padCharacter: string = "0"): string {
const numString = num.toString();
return numString.length >= width
? numString
: new Array(width - numString.length + 1).join(padCharacter) + numString;
}
private buildCommonCipher(cipher: any, c: CipherView) {
cipher.type = null;
cipher.name = c.name;
cipher.notes = c.notes;
cipher.fields = null;
cipher.reprompt = c.reprompt;
// Login props
cipher.login_uri = null;
cipher.login_username = null;
cipher.login_password = null;
cipher.login_totp = null;
if (c.fields) {
c.fields.forEach((f: any) => {
if (!cipher.fields) {
cipher.fields = "";
} else {
return this.getDecryptedExport(format);
}
}
async getOrganizationExport(organizationId: string,
format: 'csv' | 'json' | 'encrypted_json' = 'csv'): Promise<string> {
if (format === 'encrypted_json') {
return this.getOrganizationEncryptedExport(organizationId);
} else {
return this.getOrganizationDecryptedExport(organizationId, format);
}
}
async getEventExport(events: EventView[]): Promise<string> {
return papa.unparse(events.map(e => new Event(e)));
}
getFileName(prefix: string = null, extension: string = 'csv'): string {
const now = new Date();
const dateString =
now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) +
this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) +
this.padNumber(now.getSeconds(), 2);
return 'bitwarden' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.' + extension;
}
private async getDecryptedExport(format: 'json' | 'csv'): Promise<string> {
let decFolders: FolderView[] = [];
let decCiphers: CipherView[] = [];
const promises = [];
promises.push(this.folderService.getAllDecrypted().then(folders => {
decFolders = folders;
}));
promises.push(this.cipherService.getAllDecrypted().then(ciphers => {
decCiphers = ciphers.filter(f => f.deletedDate == null);
}));
await Promise.all(promises);
if (format === 'csv') {
const foldersMap = new Map<string, FolderView>();
decFolders.forEach(f => {
if (f.id != null) {
foldersMap.set(f.id, f);
}
});
const exportCiphers: any[] = [];
decCiphers.forEach(c => {
// only export logins and secure notes
if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) {
return;
}
if (c.organizationId != null) {
return;
}
const cipher: any = {};
cipher.folder = c.folderId != null && foldersMap.has(c.folderId) ?
foldersMap.get(c.folderId).name : null;
cipher.favorite = c.favorite ? 1 : null;
this.buildCommonCipher(cipher, c);
exportCiphers.push(cipher);
});
return papa.unparse(exportCiphers);
} else {
const jsonDoc: any = {
encrypted: false,
folders: [],
items: [],
};
decFolders.forEach(f => {
if (f.id == null) {
return;
}
const folder = new FolderExport();
folder.build(f);
jsonDoc.folders.push(folder);
});
decCiphers.forEach(c => {
if (c.organizationId != null) {
return;
}
const cipher = new CipherExport();
cipher.build(c);
cipher.collectionIds = null;
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, ' ');
}
}
private async getEncryptedExport(): Promise<string> {
let folders: Folder[] = [];
let ciphers: Cipher[] = [];
const promises = [];
promises.push(this.folderService.getAll().then(f => {
folders = f;
}));
promises.push(this.cipherService.getAll().then(c => {
ciphers = c.filter(f => f.deletedDate == null);
}));
await Promise.all(promises);
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid());
const jsonDoc: any = {
encrypted: true,
encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString,
folders: [],
items: [],
};
folders.forEach(f => {
if (f.id == null) {
return;
}
const folder = new FolderExport();
folder.build(f);
jsonDoc.folders.push(folder);
});
ciphers.forEach(c => {
if (c.organizationId != null) {
return;
}
const cipher = new CipherExport();
cipher.build(c);
cipher.collectionIds = null;
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, ' ');
}
private async getOrganizationDecryptedExport(organizationId: string, format: 'json' | 'csv'): Promise<string> {
const decCollections: CollectionView[] = [];
const decCiphers: CipherView[] = [];
const promises = [];
promises.push(this.apiService.getCollections(organizationId).then(collections => {
const collectionPromises: any = [];
if (collections != null && collections.data != null && collections.data.length > 0) {
collections.data.forEach(c => {
const collection = new Collection(new CollectionData(c as CollectionDetailsResponse));
collectionPromises.push(collection.decrypt().then(decCol => {
decCollections.push(decCol);
}));
});
}
return Promise.all(collectionPromises);
}));
promises.push(this.apiService.getCiphersOrganization(organizationId).then(ciphers => {
const cipherPromises: any = [];
if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) {
ciphers.data.filter(c => c.deletedDate === null).forEach(c => {
const cipher = new Cipher(new CipherData(c));
cipherPromises.push(cipher.decrypt().then(decCipher => {
decCiphers.push(decCipher);
}));
});
}
return Promise.all(cipherPromises);
}));
await Promise.all(promises);
if (format === 'csv') {
const collectionsMap = new Map<string, CollectionView>();
decCollections.forEach(c => {
collectionsMap.set(c.id, c);
});
const exportCiphers: any[] = [];
decCiphers.forEach(c => {
// only export logins and secure notes
if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) {
return;
}
const cipher: any = {};
cipher.collections = [];
if (c.collectionIds != null) {
cipher.collections = c.collectionIds.filter(id => collectionsMap.has(id))
.map(id => collectionsMap.get(id).name);
}
this.buildCommonCipher(cipher, c);
exportCiphers.push(cipher);
});
return papa.unparse(exportCiphers);
} else {
const jsonDoc: any = {
encrypted: false,
collections: [],
items: [],
};
decCollections.forEach(c => {
const collection = new CollectionExport();
collection.build(c);
jsonDoc.collections.push(collection);
});
decCiphers.forEach(c => {
const cipher = new CipherExport();
cipher.build(c);
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, ' ');
}
}
private async getOrganizationEncryptedExport(organizationId: string): Promise<string> {
const collections: Collection[] = [];
const ciphers: Cipher[] = [];
const promises = [];
promises.push(this.apiService.getCollections(organizationId).then(c => {
const collectionPromises: any = [];
if (c != null && c.data != null && c.data.length > 0) {
c.data.forEach(r => {
const collection = new Collection(new CollectionData(r as CollectionDetailsResponse));
collections.push(collection);
});
}
return Promise.all(collectionPromises);
}));
promises.push(this.apiService.getCiphersOrganization(organizationId).then(c => {
const cipherPromises: any = [];
if (c != null && c.data != null && c.data.length > 0) {
c.data.filter(item => item.deletedDate === null).forEach(item => {
const cipher = new Cipher(new CipherData(item));
ciphers.push(cipher);
});
}
return Promise.all(cipherPromises);
}));
await Promise.all(promises);
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), orgKey);
const jsonDoc: any = {
encrypted: true,
encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString,
collections: [],
items: [],
};
collections.forEach(c => {
const collection = new CollectionExport();
collection.build(c);
jsonDoc.collections.push(collection);
});
ciphers.forEach(c => {
const cipher = new CipherExport();
cipher.build(c);
jsonDoc.items.push(cipher);
});
return JSON.stringify(jsonDoc, null, ' ');
}
private padNumber(num: number, width: number, padCharacter: string = '0'): string {
const numString = num.toString();
return numString.length >= width ? numString :
new Array(width - numString.length + 1).join(padCharacter) + numString;
}
private buildCommonCipher(cipher: any, c: CipherView) {
cipher.type = null;
cipher.name = c.name;
cipher.notes = c.notes;
cipher.fields = null;
cipher.reprompt = c.reprompt;
// Login props
cipher.login_uri = null;
cipher.login_username = null;
cipher.login_password = null;
cipher.login_totp = null;
if (c.fields) {
c.fields.forEach((f: any) => {
if (!cipher.fields) {
cipher.fields = '';
} else {
cipher.fields += '\n';
}
cipher.fields += ((f.name || '') + ': ' + f.value);
});
cipher.fields += "\n";
}
switch (c.type) {
case CipherType.Login:
cipher.type = 'login';
cipher.login_username = c.login.username;
cipher.login_password = c.login.password;
cipher.login_totp = c.login.totp;
if (c.login.uris) {
cipher.login_uri = [];
c.login.uris.forEach(u => {
cipher.login_uri.push(u.uri);
});
}
break;
case CipherType.SecureNote:
cipher.type = 'note';
break;
default:
return;
}
return cipher;
cipher.fields += (f.name || "") + ": " + f.value;
});
}
switch (c.type) {
case CipherType.Login:
cipher.type = "login";
cipher.login_username = c.login.username;
cipher.login_password = c.login.password;
cipher.login_totp = c.login.totp;
if (c.login.uris) {
cipher.login_uri = [];
c.login.uris.forEach((u) => {
cipher.login_uri.push(u.uri);
});
}
break;
case CipherType.SecureNote:
cipher.type = "note";
break;
default:
return;
}
return cipher;
}
}

View File

@@ -1,79 +1,109 @@
import { ApiService } from '../abstractions/api.service';
import { FileUploadService as FileUploadServiceAbstraction } from '../abstractions/fileUpload.service';
import { LogService } from '../abstractions/log.service';
import { ApiService } from "../abstractions/api.service";
import { FileUploadService as FileUploadServiceAbstraction } from "../abstractions/fileUpload.service";
import { LogService } from "../abstractions/log.service";
import { FileUploadType } from '../enums/fileUploadType';
import { FileUploadType } from "../enums/fileUploadType";
import { EncArrayBuffer } from '../models/domain/encArrayBuffer';
import { EncString } from '../models/domain/encString';
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { EncString } from "../models/domain/encString";
import { AttachmentUploadDataResponse } from '../models/response/attachmentUploadDataResponse';
import { SendFileUploadDataResponse } from '../models/response/sendFileUploadDataResponse';
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
import { SendFileUploadDataResponse } from "../models/response/sendFileUploadDataResponse";
import { AzureFileUploadService } from './azureFileUpload.service';
import { BitwardenFileUploadService } from './bitwardenFileUpload.service';
import { AzureFileUploadService } from "./azureFileUpload.service";
import { BitwardenFileUploadService } from "./bitwardenFileUpload.service";
export class FileUploadService implements FileUploadServiceAbstraction {
private azureFileUploadService: AzureFileUploadService;
private bitwardenFileUploadService: BitwardenFileUploadService;
private azureFileUploadService: AzureFileUploadService;
private bitwardenFileUploadService: BitwardenFileUploadService;
constructor(private logService: LogService, private apiService: ApiService) {
this.azureFileUploadService = new AzureFileUploadService(logService);
this.bitwardenFileUploadService = new BitwardenFileUploadService(apiService);
}
constructor(private logService: LogService, private apiService: ApiService) {
this.azureFileUploadService = new AzureFileUploadService(logService);
this.bitwardenFileUploadService = new BitwardenFileUploadService(apiService);
}
async uploadSendFile(uploadData: SendFileUploadDataResponse, fileName: EncString, encryptedFileData: EncArrayBuffer) {
try {
switch (uploadData.fileUploadType) {
case FileUploadType.Direct:
await this.bitwardenFileUploadService.upload(fileName.encryptedString, encryptedFileData,
fd => this.apiService.postSendFile(uploadData.sendResponse.id, uploadData.sendResponse.file.id, fd));
break;
case FileUploadType.Azure:
const renewalCallback = async () => {
const renewalResponse = await this.apiService.renewSendFileUploadUrl(uploadData.sendResponse.id,
uploadData.sendResponse.file.id);
return renewalResponse.url;
};
await this.azureFileUploadService.upload(uploadData.url, encryptedFileData,
renewalCallback);
break;
default:
throw new Error('Unknown file upload type');
}
} catch (e) {
await this.apiService.deleteSend(uploadData.sendResponse.id);
throw e;
}
async uploadSendFile(
uploadData: SendFileUploadDataResponse,
fileName: EncString,
encryptedFileData: EncArrayBuffer
) {
try {
switch (uploadData.fileUploadType) {
case FileUploadType.Direct:
await this.bitwardenFileUploadService.upload(
fileName.encryptedString,
encryptedFileData,
(fd) =>
this.apiService.postSendFile(
uploadData.sendResponse.id,
uploadData.sendResponse.file.id,
fd
)
);
break;
case FileUploadType.Azure:
const renewalCallback = async () => {
const renewalResponse = await this.apiService.renewSendFileUploadUrl(
uploadData.sendResponse.id,
uploadData.sendResponse.file.id
);
return renewalResponse.url;
};
await this.azureFileUploadService.upload(
uploadData.url,
encryptedFileData,
renewalCallback
);
break;
default:
throw new Error("Unknown file upload type");
}
} catch (e) {
await this.apiService.deleteSend(uploadData.sendResponse.id);
throw e;
}
}
async uploadCipherAttachment(admin: boolean, uploadData: AttachmentUploadDataResponse, encryptedFileName: EncString,
encryptedFileData: EncArrayBuffer) {
const response = admin ? uploadData.cipherMiniResponse : uploadData.cipherResponse;
try {
switch (uploadData.fileUploadType) {
case FileUploadType.Direct:
await this.bitwardenFileUploadService.upload(encryptedFileName.encryptedString, encryptedFileData,
fd => this.apiService.postAttachmentFile(response.id, uploadData.attachmentId, fd));
break;
case FileUploadType.Azure:
const renewalCallback = async () => {
const renewalResponse = await this.apiService.renewAttachmentUploadUrl(response.id,
uploadData.attachmentId);
return renewalResponse.url;
};
await this.azureFileUploadService.upload(uploadData.url, encryptedFileData, renewalCallback);
break;
default:
throw new Error('Unknown file upload type.');
}
} catch (e) {
if (admin) {
await this.apiService.deleteCipherAttachmentAdmin(response.id, uploadData.attachmentId);
} else {
await this.apiService.deleteCipherAttachment(response.id, uploadData.attachmentId);
}
throw e;
}
async uploadCipherAttachment(
admin: boolean,
uploadData: AttachmentUploadDataResponse,
encryptedFileName: EncString,
encryptedFileData: EncArrayBuffer
) {
const response = admin ? uploadData.cipherMiniResponse : uploadData.cipherResponse;
try {
switch (uploadData.fileUploadType) {
case FileUploadType.Direct:
await this.bitwardenFileUploadService.upload(
encryptedFileName.encryptedString,
encryptedFileData,
(fd) => this.apiService.postAttachmentFile(response.id, uploadData.attachmentId, fd)
);
break;
case FileUploadType.Azure:
const renewalCallback = async () => {
const renewalResponse = await this.apiService.renewAttachmentUploadUrl(
response.id,
uploadData.attachmentId
);
return renewalResponse.url;
};
await this.azureFileUploadService.upload(
uploadData.url,
encryptedFileData,
renewalCallback
);
break;
default:
throw new Error("Unknown file upload type.");
}
} catch (e) {
if (admin) {
await this.apiService.deleteCipherAttachmentAdmin(response.id, uploadData.attachmentId);
} else {
await this.apiService.deleteCipherAttachment(response.id, uploadData.attachmentId);
}
throw e;
}
}
}

View File

@@ -1,195 +1,199 @@
import { FolderData } from '../models/data/folderData';
import { FolderData } from "../models/data/folderData";
import { Folder } from '../models/domain/folder';
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
import { TreeNode } from '../models/domain/treeNode';
import { Folder } from "../models/domain/folder";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TreeNode } from "../models/domain/treeNode";
import { FolderRequest } from '../models/request/folderRequest';
import { FolderRequest } from "../models/request/folderRequest";
import { FolderResponse } from '../models/response/folderResponse';
import { FolderResponse } from "../models/response/folderResponse";
import { FolderView } from '../models/view/folderView';
import { FolderView } from "../models/view/folderView";
import { ApiService } from '../abstractions/api.service';
import { CipherService } from '../abstractions/cipher.service';
import { CryptoService } from '../abstractions/crypto.service';
import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service';
import { I18nService } from '../abstractions/i18n.service';
import { StateService } from '../abstractions/state.service';
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { CryptoService } from "../abstractions/crypto.service";
import { FolderService as FolderServiceAbstraction } from "../abstractions/folder.service";
import { I18nService } from "../abstractions/i18n.service";
import { StateService } from "../abstractions/state.service";
import { CipherData } from '../models/data/cipherData';
import { CipherData } from "../models/data/cipherData";
import { ServiceUtils } from '../misc/serviceUtils';
import { Utils } from '../misc/utils';
import { ServiceUtils } from "../misc/serviceUtils";
import { Utils } from "../misc/utils";
const NestingDelimiter = '/';
const NestingDelimiter = "/";
export class FolderService implements FolderServiceAbstraction {
constructor(private cryptoService: CryptoService, private apiService: ApiService,
private i18nService: I18nService, private cipherService: CipherService,
private stateService: StateService) { }
constructor(
private cryptoService: CryptoService,
private apiService: ApiService,
private i18nService: I18nService,
private cipherService: CipherService,
private stateService: StateService
) {}
async clearCache(userId?: string): Promise<void> {
await this.stateService.setDecryptedFolders(null, { userId: userId });
async clearCache(userId?: string): Promise<void> {
await this.stateService.setDecryptedFolders(null, { userId: userId });
}
async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise<Folder> {
const folder = new Folder();
folder.id = model.id;
folder.name = await this.cryptoService.encrypt(model.name, key);
return folder;
}
async get(id: string): Promise<Folder> {
const folders = await this.stateService.getEncryptedFolders();
if (folders == null || !folders.hasOwnProperty(id)) {
return null;
}
async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise<Folder> {
const folder = new Folder();
folder.id = model.id;
folder.name = await this.cryptoService.encrypt(model.name, key);
return folder;
return new Folder(folders[id]);
}
async getAll(): Promise<Folder[]> {
const folders = await this.stateService.getEncryptedFolders();
const response: Folder[] = [];
for (const id in folders) {
if (folders.hasOwnProperty(id)) {
response.push(new Folder(folders[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<FolderView[]> {
const decryptedFolders = await this.stateService.getDecryptedFolders();
if (decryptedFolders != null) {
return decryptedFolders;
}
async get(id: string): Promise<Folder> {
const folders = await this.stateService.getEncryptedFolders();
if (folders == null || !folders.hasOwnProperty(id)) {
return null;
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error("No key.");
}
const decFolders: FolderView[] = [];
const promises: Promise<any>[] = [];
const folders = await this.getAll();
folders.forEach((folder) => {
promises.push(folder.decrypt().then((f) => decFolders.push(f)));
});
await Promise.all(promises);
decFolders.sort(Utils.getSortFunction(this.i18nService, "name"));
const noneFolder = new FolderView();
noneFolder.name = this.i18nService.t("noneFolder");
decFolders.push(noneFolder);
await this.stateService.setDecryptedFolders(decFolders);
return decFolders;
}
async getAllNested(): Promise<TreeNode<FolderView>[]> {
const folders = await this.getAllDecrypted();
const nodes: TreeNode<FolderView>[] = [];
folders.forEach((f) => {
const folderCopy = new FolderView();
folderCopy.id = f.id;
folderCopy.revisionDate = f.revisionDate;
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
});
return nodes;
}
async getNested(id: string): Promise<TreeNode<FolderView>> {
const folders = await this.getAllNested();
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
}
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.stateService.getUserId();
const data = new FolderData(response, userId);
await this.upsert(data);
}
async upsert(folder: FolderData | FolderData[]): Promise<any> {
let folders = await this.stateService.getEncryptedFolders();
if (folders == null) {
folders = {};
}
if (folder instanceof FolderData) {
const f = folder as FolderData;
folders[f.id] = f;
} else {
(folder as FolderData[]).forEach((f) => {
folders[f.id] = f;
});
}
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
}
async replace(folders: { [id: string]: FolderData }): Promise<any> {
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
}
async clear(userId?: string): Promise<any> {
await this.stateService.setDecryptedFolders(null, { userId: userId });
await this.stateService.setEncryptedFolders(null, { userId: userId });
}
async delete(id: string | string[]): Promise<any> {
const folders = await this.stateService.getEncryptedFolders();
if (folders == null) {
return;
}
if (typeof id === "string") {
if (folders[id] == null) {
return;
}
delete folders[id];
} else {
(id as string[]).forEach((i) => {
delete folders[i];
});
}
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
// Items in a deleted folder are re-assigned to "No Folder"
const ciphers = await this.stateService.getEncryptedCiphers();
if (ciphers != null) {
const updates: CipherData[] = [];
for (const cId in ciphers) {
if (ciphers[cId].folderId === id) {
ciphers[cId].folderId = null;
updates.push(ciphers[cId]);
}
return new Folder(folders[id]);
}
if (updates.length > 0) {
this.cipherService.upsert(updates);
}
}
}
async getAll(): Promise<Folder[]> {
const folders = await this.stateService.getEncryptedFolders();
const response: Folder[] = [];
for (const id in folders) {
if (folders.hasOwnProperty(id)) {
response.push(new Folder(folders[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<FolderView[]> {
const decryptedFolders = await this.stateService.getDecryptedFolders();
if (decryptedFolders != null) {
return decryptedFolders;
}
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error('No key.');
}
const decFolders: FolderView[] = [];
const promises: Promise<any>[] = [];
const folders = await this.getAll();
folders.forEach(folder => {
promises.push(folder.decrypt().then(f => decFolders.push(f)));
});
await Promise.all(promises);
decFolders.sort(Utils.getSortFunction(this.i18nService, 'name'));
const noneFolder = new FolderView();
noneFolder.name = this.i18nService.t('noneFolder');
decFolders.push(noneFolder);
await this.stateService.setDecryptedFolders(decFolders);
return decFolders;
}
async getAllNested(): Promise<TreeNode<FolderView>[]> {
const folders = await this.getAllDecrypted();
const nodes: TreeNode<FolderView>[] = [];
folders.forEach(f => {
const folderCopy = new FolderView();
folderCopy.id = f.id;
folderCopy.revisionDate = f.revisionDate;
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
});
return nodes;
}
async getNested(id: string): Promise<TreeNode<FolderView>> {
const folders = await this.getAllNested();
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
}
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.stateService.getUserId();
const data = new FolderData(response, userId);
await this.upsert(data);
}
async upsert(folder: FolderData | FolderData[]): Promise<any> {
let folders = await this.stateService.getEncryptedFolders();
if (folders == null) {
folders = {};
}
if (folder instanceof FolderData) {
const f = folder as FolderData;
folders[f.id] = f;
} else {
(folder as FolderData[]).forEach(f => {
folders[f.id] = f;
});
}
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
}
async replace(folders: { [id: string]: FolderData; }): Promise<any> {
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
}
async clear(userId?: string): Promise<any> {
await this.stateService.setDecryptedFolders(null, { userId: userId });
await this.stateService.setEncryptedFolders(null, { userId: userId });
}
async delete(id: string | string[]): Promise<any> {
const folders = await this.stateService.getEncryptedFolders();
if (folders == null) {
return;
}
if (typeof id === 'string') {
if (folders[id] == null) {
return;
}
delete folders[id];
} else {
(id as string[]).forEach(i => {
delete folders[i];
});
}
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
// Items in a deleted folder are re-assigned to "No Folder"
const ciphers = await this.stateService.getEncryptedCiphers();
if (ciphers != null) {
const updates: CipherData[] = [];
for (const cId in ciphers) {
if (ciphers[cId].folderId === id) {
ciphers[cId].folderId = null;
updates.push(ciphers[cId]);
}
}
if (updates.length > 0) {
this.cipherService.upsert(updates);
}
}
}
async deleteWithServer(id: string): Promise<any> {
await this.apiService.deleteFolder(id);
await this.delete(id);
}
async deleteWithServer(id: string): Promise<any> {
await this.apiService.deleteFolder(id);
await this.delete(id);
}
}

View File

@@ -1,153 +1,160 @@
import { I18nService as I18nServiceAbstraction } from '../abstractions/i18n.service';
import { I18nService as I18nServiceAbstraction } from "../abstractions/i18n.service";
export class I18nService implements I18nServiceAbstraction {
locale: string;
// First locale is the default (English)
supportedTranslationLocales: string[] = ['en'];
translationLocale: string;
collator: Intl.Collator;
localeNames = new Map<string, string>([
['af', 'Afrikaans'],
['az', 'Azərbaycanca'],
['be', 'Беларуская'],
['bg', 'български'],
['ca', 'català'],
['cs', 'čeština'],
['da', 'dansk'],
['de', 'Deutsch'],
['el', 'Ελληνικά'],
['en', 'English'],
['en-GB', 'English (British)'],
['eo', 'Esperanto'],
['es', 'español'],
['et', 'eesti'],
['fa', 'فارسی'],
['fi', 'suomi'],
['fr', 'français'],
['he', 'עברית'],
['hi', 'हिन्दी'],
['hr', 'hrvatski'],
['hu', 'magyar'],
['id', 'Bahasa Indonesia'],
['it', 'italiano'],
['ja', '日本語'],
['ko', '한국어'],
['lv', 'Latvietis'],
['ml', 'മലയാളം'],
['nb', 'norsk (bokmål)'],
['nl', 'Nederlands'],
['pl', 'polski'],
['pt-BR', 'português do Brasil'],
['pt-PT', 'português'],
['ro', 'română'],
['ru', 'русский'],
['sk', 'slovenčina'],
['sr', 'Српски'],
['sv', 'svenska'],
['th', 'ไทย'],
['tr', 'Türkçe'],
['uk', 'українська'],
['vi', 'Tiếng Việt'],
['zh-CN', '中文(中国大陆)'],
['zh-TW', '中文(台灣)'],
]);
locale: string;
// First locale is the default (English)
supportedTranslationLocales: string[] = ["en"];
translationLocale: string;
collator: Intl.Collator;
localeNames = new Map<string, string>([
["af", "Afrikaans"],
["az", "Azərbaycanca"],
["be", "Беларуская"],
["bg", "български"],
["ca", "català"],
["cs", "čeština"],
["da", "dansk"],
["de", "Deutsch"],
["el", "Ελληνικά"],
["en", "English"],
["en-GB", "English (British)"],
["eo", "Esperanto"],
["es", "español"],
["et", "eesti"],
["fa", "فارسی"],
["fi", "suomi"],
["fr", "français"],
["he", "עברית"],
["hi", "हिन्दी"],
["hr", "hrvatski"],
["hu", "magyar"],
["id", "Bahasa Indonesia"],
["it", "italiano"],
["ja", "日本語"],
["ko", "한국어"],
["lv", "Latvietis"],
["ml", "മലയാളം"],
["nb", "norsk (bokmål)"],
["nl", "Nederlands"],
["pl", "polski"],
["pt-BR", "português do Brasil"],
["pt-PT", "português"],
["ro", "română"],
["ru", "русский"],
["sk", "slovenčina"],
["sr", "Српски"],
["sv", "svenska"],
["th", "ไทย"],
["tr", "Türkçe"],
["uk", "українська"],
["vi", "Tiếng Việt"],
["zh-CN", "中文(中国大陆)"],
["zh-TW", "中文(台灣)"],
]);
protected inited: boolean;
protected defaultMessages: any = {};
protected localeMessages: any = {};
protected inited: boolean;
protected defaultMessages: any = {};
protected localeMessages: any = {};
constructor(protected systemLanguage: string, protected localesDirectory: string,
protected getLocalesJson: (formattedLocale: string) => Promise<any>) {
this.systemLanguage = systemLanguage.replace('_', '-');
constructor(
protected systemLanguage: string,
protected localesDirectory: string,
protected getLocalesJson: (formattedLocale: string) => Promise<any>
) {
this.systemLanguage = systemLanguage.replace("_", "-");
}
async init(locale?: string) {
if (this.inited) {
throw new Error("i18n already initialized.");
}
if (this.supportedTranslationLocales == null || this.supportedTranslationLocales.length === 0) {
throw new Error("supportedTranslationLocales not set.");
}
async init(locale?: string) {
if (this.inited) {
throw new Error('i18n already initialized.');
}
if (this.supportedTranslationLocales == null || this.supportedTranslationLocales.length === 0) {
throw new Error('supportedTranslationLocales not set.');
}
this.inited = true;
this.locale = this.translationLocale = locale != null ? locale : this.systemLanguage;
this.inited = true;
this.locale = this.translationLocale = locale != null ? locale : this.systemLanguage;
try {
this.collator = new Intl.Collator(this.locale, { numeric: true, sensitivity: 'base' });
} catch {
this.collator = null;
}
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
this.translationLocale = this.translationLocale.slice(0, 2);
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
this.translationLocale = this.supportedTranslationLocales[0];
}
}
if (this.localesDirectory != null) {
await this.loadMessages(this.translationLocale, this.localeMessages);
if (this.translationLocale !== this.supportedTranslationLocales[0]) {
await this.loadMessages(this.supportedTranslationLocales[0], this.defaultMessages);
}
}
try {
this.collator = new Intl.Collator(this.locale, { numeric: true, sensitivity: "base" });
} catch {
this.collator = null;
}
t(id: string, p1?: string, p2?: string, p3?: string): string {
return this.translate(id, p1, p2, p3);
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
this.translationLocale = this.translationLocale.slice(0, 2);
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
this.translationLocale = this.supportedTranslationLocales[0];
}
}
translate(id: string, p1?: string, p2?: string, p3?: string): string {
let result: string;
if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) {
result = this.localeMessages[id];
} else if (this.defaultMessages.hasOwnProperty(id) && this.defaultMessages[id]) {
result = this.defaultMessages[id];
} else {
result = '';
}
if (this.localesDirectory != null) {
await this.loadMessages(this.translationLocale, this.localeMessages);
if (this.translationLocale !== this.supportedTranslationLocales[0]) {
await this.loadMessages(this.supportedTranslationLocales[0], this.defaultMessages);
}
}
}
if (result !== '') {
if (p1 != null) {
result = result.split('__$1__').join(p1);
}
if (p2 != null) {
result = result.split('__$2__').join(p2);
}
if (p3 != null) {
result = result.split('__$3__').join(p3);
}
}
t(id: string, p1?: string, p2?: string, p3?: string): string {
return this.translate(id, p1, p2, p3);
}
return result;
translate(id: string, p1?: string, p2?: string, p3?: string): string {
let result: string;
if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) {
result = this.localeMessages[id];
} else if (this.defaultMessages.hasOwnProperty(id) && this.defaultMessages[id]) {
result = this.defaultMessages[id];
} else {
result = "";
}
private async loadMessages(locale: string, messagesObj: any): Promise<any> {
const formattedLocale = locale.replace('-', '_');
const locales = await this.getLocalesJson(formattedLocale);
for (const prop in locales) {
if (!locales.hasOwnProperty(prop)) {
continue;
}
messagesObj[prop] = locales[prop].message;
if (locales[prop].placeholders) {
for (const placeProp in locales[prop].placeholders) {
if (!locales[prop].placeholders.hasOwnProperty(placeProp) ||
!locales[prop].placeholders[placeProp].content) {
continue;
}
const replaceToken = '\\$' + placeProp.toUpperCase() + '\\$';
let replaceContent = locales[prop].placeholders[placeProp].content;
if (replaceContent === '$1' || replaceContent === '$2' || replaceContent === '$3') {
replaceContent = '__$' + replaceContent + '__';
}
messagesObj[prop] = messagesObj[prop].replace(new RegExp(replaceToken, 'g'), replaceContent);
}
}
}
if (result !== "") {
if (p1 != null) {
result = result.split("__$1__").join(p1);
}
if (p2 != null) {
result = result.split("__$2__").join(p2);
}
if (p3 != null) {
result = result.split("__$3__").join(p3);
}
}
return result;
}
private async loadMessages(locale: string, messagesObj: any): Promise<any> {
const formattedLocale = locale.replace("-", "_");
const locales = await this.getLocalesJson(formattedLocale);
for (const prop in locales) {
if (!locales.hasOwnProperty(prop)) {
continue;
}
messagesObj[prop] = locales[prop].message;
if (locales[prop].placeholders) {
for (const placeProp in locales[prop].placeholders) {
if (
!locales[prop].placeholders.hasOwnProperty(placeProp) ||
!locales[prop].placeholders[placeProp].content
) {
continue;
}
const replaceToken = "\\$" + placeProp.toUpperCase() + "\\$";
let replaceContent = locales[prop].placeholders[placeProp].content;
if (replaceContent === "$1" || replaceContent === "$2" || replaceContent === "$3") {
replaceContent = "__$" + replaceContent + "__";
}
messagesObj[prop] = messagesObj[prop].replace(
new RegExp(replaceToken, "g"),
replaceContent
);
}
}
}
}
}

View File

@@ -1,403 +1,420 @@
import { ApiService } from '../abstractions/api.service';
import { CipherService } from '../abstractions/cipher.service';
import { CollectionService } from '../abstractions/collection.service';
import { CryptoService } from '../abstractions/crypto.service';
import { FolderService } from '../abstractions/folder.service';
import { I18nService } from '../abstractions/i18n.service';
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { CollectionService } from "../abstractions/collection.service";
import { CryptoService } from "../abstractions/crypto.service";
import { FolderService } from "../abstractions/folder.service";
import { I18nService } from "../abstractions/i18n.service";
import {
ImportOption,
ImportService as ImportServiceAbstraction,
} from '../abstractions/import.service';
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
ImportOption,
ImportService as ImportServiceAbstraction,
} from "../abstractions/import.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import { ImportResult } from '../models/domain/importResult';
import { ImportResult } from "../models/domain/importResult";
import { CipherType } from '../enums/cipherType';
import { CipherType } from "../enums/cipherType";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
import { CipherRequest } from '../models/request/cipherRequest';
import { CollectionRequest } from '../models/request/collectionRequest';
import { FolderRequest } from '../models/request/folderRequest';
import { ImportCiphersRequest } from '../models/request/importCiphersRequest';
import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest';
import { KvpRequest } from '../models/request/kvpRequest';
import { CipherRequest } from "../models/request/cipherRequest";
import { CollectionRequest } from "../models/request/collectionRequest";
import { FolderRequest } from "../models/request/folderRequest";
import { ImportCiphersRequest } from "../models/request/importCiphersRequest";
import { ImportOrganizationCiphersRequest } from "../models/request/importOrganizationCiphersRequest";
import { KvpRequest } from "../models/request/kvpRequest";
import { ErrorResponse } from '../models/response/errorResponse';
import { CipherView } from '../models/view/cipherView';
import { ErrorResponse } from "../models/response/errorResponse";
import { CipherView } from "../models/view/cipherView";
import { AscendoCsvImporter } from '../importers/ascendoCsvImporter';
import { AvastCsvImporter } from '../importers/avastCsvImporter';
import { AvastJsonImporter } from '../importers/avastJsonImporter';
import { AviraCsvImporter } from '../importers/aviraCsvImporter';
import { BitwardenCsvImporter } from '../importers/bitwardenCsvImporter';
import { BitwardenJsonImporter } from '../importers/bitwardenJsonImporter';
import { BlackBerryCsvImporter } from '../importers/blackBerryCsvImporter';
import { BlurCsvImporter } from '../importers/blurCsvImporter';
import { ButtercupCsvImporter } from '../importers/buttercupCsvImporter';
import { ChromeCsvImporter } from '../importers/chromeCsvImporter';
import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter';
import { CodebookCsvImporter } from '../importers/codebookCsvImporter';
import { DashlaneJsonImporter } from '../importers/dashlaneJsonImporter';
import { EncryptrCsvImporter } from '../importers/encryptrCsvImporter';
import { EnpassCsvImporter } from '../importers/enpassCsvImporter';
import { EnpassJsonImporter } from '../importers/enpassJsonImporter';
import { FirefoxCsvImporter } from '../importers/firefoxCsvImporter';
import { FSecureFskImporter } from '../importers/fsecureFskImporter';
import { GnomeJsonImporter } from '../importers/gnomeJsonImporter';
import { Importer } from '../importers/importer';
import { KasperskyTxtImporter } from '../importers/kasperskyTxtImporter';
import { KeePass2XmlImporter } from '../importers/keepass2XmlImporter';
import { KeePassXCsvImporter } from '../importers/keepassxCsvImporter';
import { KeeperCsvImporter } from '../importers/keeperCsvImporter';
import { LastPassCsvImporter } from '../importers/lastpassCsvImporter';
import { LogMeOnceCsvImporter } from '../importers/logMeOnceCsvImporter';
import { MeldiumCsvImporter } from '../importers/meldiumCsvImporter';
import { MSecureCsvImporter } from '../importers/msecureCsvImporter';
import { MykiCsvImporter } from '../importers/mykiCsvImporter';
import { NordPassCsvImporter } from '../importers/nordpassCsvImporter';
import { OnePassword1PifImporter } from '../importers/onepasswordImporters/onepassword1PifImporter';
import { OnePasswordMacCsvImporter } from '../importers/onepasswordImporters/onepasswordMacCsvImporter';
import { OnePasswordWinCsvImporter } from '../importers/onepasswordImporters/onepasswordWinCsvImporter';
import { PadlockCsvImporter } from '../importers/padlockCsvImporter';
import { PassKeepCsvImporter } from '../importers/passkeepCsvImporter';
import { PassmanJsonImporter } from '../importers/passmanJsonImporter';
import { PasspackCsvImporter } from '../importers/passpackCsvImporter';
import { PasswordAgentCsvImporter } from '../importers/passwordAgentCsvImporter';
import { PasswordBossJsonImporter } from '../importers/passwordBossJsonImporter';
import { PasswordDragonXmlImporter } from '../importers/passwordDragonXmlImporter';
import { PasswordSafeXmlImporter } from '../importers/passwordSafeXmlImporter';
import { PasswordWalletTxtImporter } from '../importers/passwordWalletTxtImporter';
import { RememBearCsvImporter } from '../importers/rememBearCsvImporter';
import { RoboFormCsvImporter } from '../importers/roboformCsvImporter';
import { SafariCsvImporter } from '../importers/safariCsvImporter';
import { SafeInCloudXmlImporter } from '../importers/safeInCloudXmlImporter';
import { SaferPassCsvImporter } from '../importers/saferpassCsvImport';
import { SecureSafeCsvImporter } from '../importers/secureSafeCsvImporter';
import { SplashIdCsvImporter } from '../importers/splashIdCsvImporter';
import { StickyPasswordXmlImporter } from '../importers/stickyPasswordXmlImporter';
import { TrueKeyCsvImporter } from '../importers/truekeyCsvImporter';
import { UpmCsvImporter } from '../importers/upmCsvImporter';
import { YotiCsvImporter } from '../importers/yotiCsvImporter';
import { ZohoVaultCsvImporter } from '../importers/zohoVaultCsvImporter';
import { AscendoCsvImporter } from "../importers/ascendoCsvImporter";
import { AvastCsvImporter } from "../importers/avastCsvImporter";
import { AvastJsonImporter } from "../importers/avastJsonImporter";
import { AviraCsvImporter } from "../importers/aviraCsvImporter";
import { BitwardenCsvImporter } from "../importers/bitwardenCsvImporter";
import { BitwardenJsonImporter } from "../importers/bitwardenJsonImporter";
import { BlackBerryCsvImporter } from "../importers/blackBerryCsvImporter";
import { BlurCsvImporter } from "../importers/blurCsvImporter";
import { ButtercupCsvImporter } from "../importers/buttercupCsvImporter";
import { ChromeCsvImporter } from "../importers/chromeCsvImporter";
import { ClipperzHtmlImporter } from "../importers/clipperzHtmlImporter";
import { CodebookCsvImporter } from "../importers/codebookCsvImporter";
import { DashlaneJsonImporter } from "../importers/dashlaneJsonImporter";
import { EncryptrCsvImporter } from "../importers/encryptrCsvImporter";
import { EnpassCsvImporter } from "../importers/enpassCsvImporter";
import { EnpassJsonImporter } from "../importers/enpassJsonImporter";
import { FirefoxCsvImporter } from "../importers/firefoxCsvImporter";
import { FSecureFskImporter } from "../importers/fsecureFskImporter";
import { GnomeJsonImporter } from "../importers/gnomeJsonImporter";
import { Importer } from "../importers/importer";
import { KasperskyTxtImporter } from "../importers/kasperskyTxtImporter";
import { KeePass2XmlImporter } from "../importers/keepass2XmlImporter";
import { KeePassXCsvImporter } from "../importers/keepassxCsvImporter";
import { KeeperCsvImporter } from "../importers/keeperCsvImporter";
import { LastPassCsvImporter } from "../importers/lastpassCsvImporter";
import { LogMeOnceCsvImporter } from "../importers/logMeOnceCsvImporter";
import { MeldiumCsvImporter } from "../importers/meldiumCsvImporter";
import { MSecureCsvImporter } from "../importers/msecureCsvImporter";
import { MykiCsvImporter } from "../importers/mykiCsvImporter";
import { NordPassCsvImporter } from "../importers/nordpassCsvImporter";
import { OnePassword1PifImporter } from "../importers/onepasswordImporters/onepassword1PifImporter";
import { OnePasswordMacCsvImporter } from "../importers/onepasswordImporters/onepasswordMacCsvImporter";
import { OnePasswordWinCsvImporter } from "../importers/onepasswordImporters/onepasswordWinCsvImporter";
import { PadlockCsvImporter } from "../importers/padlockCsvImporter";
import { PassKeepCsvImporter } from "../importers/passkeepCsvImporter";
import { PassmanJsonImporter } from "../importers/passmanJsonImporter";
import { PasspackCsvImporter } from "../importers/passpackCsvImporter";
import { PasswordAgentCsvImporter } from "../importers/passwordAgentCsvImporter";
import { PasswordBossJsonImporter } from "../importers/passwordBossJsonImporter";
import { PasswordDragonXmlImporter } from "../importers/passwordDragonXmlImporter";
import { PasswordSafeXmlImporter } from "../importers/passwordSafeXmlImporter";
import { PasswordWalletTxtImporter } from "../importers/passwordWalletTxtImporter";
import { RememBearCsvImporter } from "../importers/rememBearCsvImporter";
import { RoboFormCsvImporter } from "../importers/roboformCsvImporter";
import { SafariCsvImporter } from "../importers/safariCsvImporter";
import { SafeInCloudXmlImporter } from "../importers/safeInCloudXmlImporter";
import { SaferPassCsvImporter } from "../importers/saferpassCsvImport";
import { SecureSafeCsvImporter } from "../importers/secureSafeCsvImporter";
import { SplashIdCsvImporter } from "../importers/splashIdCsvImporter";
import { StickyPasswordXmlImporter } from "../importers/stickyPasswordXmlImporter";
import { TrueKeyCsvImporter } from "../importers/truekeyCsvImporter";
import { UpmCsvImporter } from "../importers/upmCsvImporter";
import { YotiCsvImporter } from "../importers/yotiCsvImporter";
import { ZohoVaultCsvImporter } from "../importers/zohoVaultCsvImporter";
export class ImportService implements ImportServiceAbstraction {
featuredImportOptions = [
{ id: 'bitwardenjson', name: 'Bitwarden (json)' },
{ id: 'bitwardencsv', name: 'Bitwarden (csv)' },
{ id: 'chromecsv', name: 'Chrome (csv)' },
{ id: 'dashlanejson', name: 'Dashlane (json)' },
{ id: 'firefoxcsv', name: 'Firefox (csv)' },
{ id: 'keepass2xml', name: 'KeePass 2 (xml)' },
{ id: 'lastpasscsv', name: 'LastPass (csv)' },
{ id: 'safaricsv', name: 'Safari and macOS (csv)' },
{ id: '1password1pif', name: '1Password (1pif)' },
];
featuredImportOptions = [
{ id: "bitwardenjson", name: "Bitwarden (json)" },
{ id: "bitwardencsv", name: "Bitwarden (csv)" },
{ id: "chromecsv", name: "Chrome (csv)" },
{ id: "dashlanejson", name: "Dashlane (json)" },
{ id: "firefoxcsv", name: "Firefox (csv)" },
{ id: "keepass2xml", name: "KeePass 2 (xml)" },
{ id: "lastpasscsv", name: "LastPass (csv)" },
{ id: "safaricsv", name: "Safari and macOS (csv)" },
{ id: "1password1pif", name: "1Password (1pif)" },
];
regularImportOptions: ImportOption[] = [
{ id: 'keepassxcsv', name: 'KeePassX (csv)' },
{ id: '1passwordwincsv', name: '1Password 6 and 7 Windows (csv)' },
{ id: '1passwordmaccsv', name: '1Password 6 and 7 Mac (csv)' },
{ id: 'roboformcsv', name: 'RoboForm (csv)' },
{ id: 'keepercsv', name: 'Keeper (csv)' },
{ id: 'enpasscsv', name: 'Enpass (csv)' },
{ id: 'enpassjson', name: 'Enpass (json)' },
{ id: 'safeincloudxml', name: 'SafeInCloud (xml)' },
{ id: 'pwsafexml', name: 'Password Safe (xml)' },
{ id: 'stickypasswordxml', name: 'Sticky Password (xml)' },
{ id: 'msecurecsv', name: 'mSecure (csv)' },
{ id: 'truekeycsv', name: 'True Key (csv)' },
{ id: 'passwordbossjson', name: 'Password Boss (json)' },
{ id: 'zohovaultcsv', name: 'Zoho Vault (csv)' },
{ id: 'splashidcsv', name: 'SplashID (csv)' },
{ id: 'passworddragonxml', name: 'Password Dragon (xml)' },
{ id: 'padlockcsv', name: 'Padlock (csv)' },
{ id: 'passboltcsv', name: 'Passbolt (csv)' },
{ id: 'clipperzhtml', name: 'Clipperz (html)' },
{ id: 'aviracsv', name: 'Avira (csv)' },
{ id: 'saferpasscsv', name: 'SaferPass (csv)' },
{ id: 'upmcsv', name: 'Universal Password Manager (csv)' },
{ id: 'ascendocsv', name: 'Ascendo DataVault (csv)' },
{ id: 'meldiumcsv', name: 'Meldium (csv)' },
{ id: 'passkeepcsv', name: 'PassKeep (csv)' },
{ id: 'operacsv', name: 'Opera (csv)' },
{ id: 'vivaldicsv', name: 'Vivaldi (csv)' },
{ id: 'gnomejson', name: 'GNOME Passwords and Keys/Seahorse (json)' },
{ id: 'blurcsv', name: 'Blur (csv)' },
{ id: 'passwordagentcsv', name: 'Password Agent (csv)' },
{ id: 'passpackcsv', name: 'Passpack (csv)' },
{ id: 'passmanjson', name: 'Passman (json)' },
{ id: 'avastcsv', name: 'Avast Passwords (csv)' },
{ id: 'avastjson', name: 'Avast Passwords (json)' },
{ id: 'fsecurefsk', name: 'F-Secure KEY (fsk)' },
{ id: 'kasperskytxt', name: 'Kaspersky Password Manager (txt)' },
{ id: 'remembearcsv', name: 'RememBear (csv)' },
{ id: 'passwordwallettxt', name: 'PasswordWallet (txt)' },
{ id: 'mykicsv', name: 'Myki (csv)' },
{ id: 'securesafecsv', name: 'SecureSafe (csv)' },
{ id: 'logmeoncecsv', name: 'LogMeOnce (csv)' },
{ id: 'blackberrycsv', name: 'BlackBerry Password Keeper (csv)' },
{ id: 'buttercupcsv', name: 'Buttercup (csv)' },
{ id: 'codebookcsv', name: 'Codebook (csv)' },
{ id: 'encryptrcsv', name: 'Encryptr (csv)' },
{ id: 'yoticsv', name: 'Yoti (csv)' },
{ id: 'nordpasscsv', name: 'Nordpass (csv)' },
];
regularImportOptions: ImportOption[] = [
{ id: "keepassxcsv", name: "KeePassX (csv)" },
{ id: "1passwordwincsv", name: "1Password 6 and 7 Windows (csv)" },
{ id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" },
{ id: "roboformcsv", name: "RoboForm (csv)" },
{ id: "keepercsv", name: "Keeper (csv)" },
{ id: "enpasscsv", name: "Enpass (csv)" },
{ id: "enpassjson", name: "Enpass (json)" },
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
{ id: "pwsafexml", name: "Password Safe (xml)" },
{ id: "stickypasswordxml", name: "Sticky Password (xml)" },
{ id: "msecurecsv", name: "mSecure (csv)" },
{ id: "truekeycsv", name: "True Key (csv)" },
{ id: "passwordbossjson", name: "Password Boss (json)" },
{ id: "zohovaultcsv", name: "Zoho Vault (csv)" },
{ id: "splashidcsv", name: "SplashID (csv)" },
{ id: "passworddragonxml", name: "Password Dragon (xml)" },
{ id: "padlockcsv", name: "Padlock (csv)" },
{ id: "passboltcsv", name: "Passbolt (csv)" },
{ id: "clipperzhtml", name: "Clipperz (html)" },
{ id: "aviracsv", name: "Avira (csv)" },
{ id: "saferpasscsv", name: "SaferPass (csv)" },
{ id: "upmcsv", name: "Universal Password Manager (csv)" },
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
{ id: "meldiumcsv", name: "Meldium (csv)" },
{ id: "passkeepcsv", name: "PassKeep (csv)" },
{ id: "operacsv", name: "Opera (csv)" },
{ id: "vivaldicsv", name: "Vivaldi (csv)" },
{ id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" },
{ id: "blurcsv", name: "Blur (csv)" },
{ id: "passwordagentcsv", name: "Password Agent (csv)" },
{ id: "passpackcsv", name: "Passpack (csv)" },
{ id: "passmanjson", name: "Passman (json)" },
{ id: "avastcsv", name: "Avast Passwords (csv)" },
{ id: "avastjson", name: "Avast Passwords (json)" },
{ id: "fsecurefsk", name: "F-Secure KEY (fsk)" },
{ id: "kasperskytxt", name: "Kaspersky Password Manager (txt)" },
{ id: "remembearcsv", name: "RememBear (csv)" },
{ id: "passwordwallettxt", name: "PasswordWallet (txt)" },
{ id: "mykicsv", name: "Myki (csv)" },
{ id: "securesafecsv", name: "SecureSafe (csv)" },
{ id: "logmeoncecsv", name: "LogMeOnce (csv)" },
{ id: "blackberrycsv", name: "BlackBerry Password Keeper (csv)" },
{ id: "buttercupcsv", name: "Buttercup (csv)" },
{ id: "codebookcsv", name: "Codebook (csv)" },
{ id: "encryptrcsv", name: "Encryptr (csv)" },
{ id: "yoticsv", name: "Yoti (csv)" },
{ id: "nordpasscsv", name: "Nordpass (csv)" },
];
constructor(private cipherService: CipherService, private folderService: FolderService,
private apiService: ApiService, private i18nService: I18nService,
private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService) { }
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private apiService: ApiService,
private i18nService: I18nService,
private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService
) {}
getImportOptions(): ImportOption[] {
return this.featuredImportOptions.concat(this.regularImportOptions);
}
getImportOptions(): ImportOption[] {
return this.featuredImportOptions.concat(this.regularImportOptions);
}
async import(importer: Importer, fileContents: string, organizationId: string = null): Promise<Error> {
const importResult = await importer.parse(fileContents);
if (importResult.success) {
if (importResult.folders.length === 0 && importResult.ciphers.length === 0) {
return new Error(this.i18nService.t('importNothingError'));
} else if (importResult.ciphers.length > 0) {
const halfway = Math.floor(importResult.ciphers.length / 2);
const last = importResult.ciphers.length - 1;
async import(
importer: Importer,
fileContents: string,
organizationId: string = null
): Promise<Error> {
const importResult = await importer.parse(fileContents);
if (importResult.success) {
if (importResult.folders.length === 0 && importResult.ciphers.length === 0) {
return new Error(this.i18nService.t("importNothingError"));
} else if (importResult.ciphers.length > 0) {
const halfway = Math.floor(importResult.ciphers.length / 2);
const last = importResult.ciphers.length - 1;
if (this.badData(importResult.ciphers[0]) &&
this.badData(importResult.ciphers[halfway]) &&
this.badData(importResult.ciphers[last])) {
return new Error(this.i18nService.t('importFormatError'));
}
}
try {
await this.postImport(importResult, organizationId);
} catch (error) {
const errorResponse = new ErrorResponse(error, 400);
return this.handleServerError(errorResponse, importResult);
}
return null;
} else {
if (!Utils.isNullOrWhitespace(importResult.errorMessage)) {
return new Error(importResult.errorMessage);
} else {
return new Error(this.i18nService.t('importFormatError'));
}
if (
this.badData(importResult.ciphers[0]) &&
this.badData(importResult.ciphers[halfway]) &&
this.badData(importResult.ciphers[last])
) {
return new Error(this.i18nService.t("importFormatError"));
}
}
try {
await this.postImport(importResult, organizationId);
} catch (error) {
const errorResponse = new ErrorResponse(error, 400);
return this.handleServerError(errorResponse, importResult);
}
return null;
} else {
if (!Utils.isNullOrWhitespace(importResult.errorMessage)) {
return new Error(importResult.errorMessage);
} else {
return new Error(this.i18nService.t("importFormatError"));
}
}
}
getImporter(format: string, organizationId: string = null): Importer {
const importer = this.getImporterInstance(format);
if (importer == null) {
return null;
}
importer.organizationId = organizationId;
return importer;
}
private getImporterInstance(format: string) {
if (format == null || format === "") {
return null;
}
getImporter(format: string, organizationId: string = null): Importer {
const importer = this.getImporterInstance(format);
if (importer == null) {
return null;
switch (format) {
case "bitwardencsv":
return new BitwardenCsvImporter();
case "bitwardenjson":
return new BitwardenJsonImporter(this.cryptoService, this.i18nService);
case "lastpasscsv":
case "passboltcsv":
return new LastPassCsvImporter();
case "keepassxcsv":
return new KeePassXCsvImporter();
case "aviracsv":
return new AviraCsvImporter();
case "blurcsv":
return new BlurCsvImporter();
case "safeincloudxml":
return new SafeInCloudXmlImporter();
case "padlockcsv":
return new PadlockCsvImporter();
case "keepass2xml":
return new KeePass2XmlImporter();
case "chromecsv":
case "operacsv":
case "vivaldicsv":
return new ChromeCsvImporter();
case "firefoxcsv":
return new FirefoxCsvImporter();
case "upmcsv":
return new UpmCsvImporter();
case "saferpasscsv":
return new SaferPassCsvImporter();
case "safaricsv":
return new SafariCsvImporter();
case "meldiumcsv":
return new MeldiumCsvImporter();
case "1password1pif":
return new OnePassword1PifImporter();
case "1passwordwincsv":
return new OnePasswordWinCsvImporter();
case "1passwordmaccsv":
return new OnePasswordMacCsvImporter();
case "keepercsv":
return new KeeperCsvImporter();
case "passworddragonxml":
return new PasswordDragonXmlImporter();
case "enpasscsv":
return new EnpassCsvImporter();
case "enpassjson":
return new EnpassJsonImporter();
case "pwsafexml":
return new PasswordSafeXmlImporter();
case "dashlanejson":
return new DashlaneJsonImporter();
case "msecurecsv":
return new MSecureCsvImporter();
case "stickypasswordxml":
return new StickyPasswordXmlImporter();
case "truekeycsv":
return new TrueKeyCsvImporter();
case "clipperzhtml":
return new ClipperzHtmlImporter();
case "roboformcsv":
return new RoboFormCsvImporter();
case "ascendocsv":
return new AscendoCsvImporter();
case "passwordbossjson":
return new PasswordBossJsonImporter();
case "zohovaultcsv":
return new ZohoVaultCsvImporter();
case "splashidcsv":
return new SplashIdCsvImporter();
case "passkeepcsv":
return new PassKeepCsvImporter();
case "gnomejson":
return new GnomeJsonImporter();
case "passwordagentcsv":
return new PasswordAgentCsvImporter();
case "passpackcsv":
return new PasspackCsvImporter();
case "passmanjson":
return new PassmanJsonImporter();
case "avastcsv":
return new AvastCsvImporter();
case "avastjson":
return new AvastJsonImporter();
case "fsecurefsk":
return new FSecureFskImporter();
case "kasperskytxt":
return new KasperskyTxtImporter();
case "remembearcsv":
return new RememBearCsvImporter();
case "passwordwallettxt":
return new PasswordWalletTxtImporter();
case "mykicsv":
return new MykiCsvImporter();
case "securesafecsv":
return new SecureSafeCsvImporter();
case "logmeoncecsv":
return new LogMeOnceCsvImporter();
case "blackberrycsv":
return new BlackBerryCsvImporter();
case "buttercupcsv":
return new ButtercupCsvImporter();
case "codebookcsv":
return new CodebookCsvImporter();
case "encryptrcsv":
return new EncryptrCsvImporter();
case "yoticsv":
return new YotiCsvImporter();
case "nordpasscsv":
return new NordPassCsvImporter();
default:
return null;
}
}
private async postImport(importResult: ImportResult, organizationId: string = null) {
if (organizationId == null) {
const request = new ImportCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
request.ciphers.push(new CipherRequest(c));
}
if (importResult.folders != null) {
for (let i = 0; i < importResult.folders.length; i++) {
const f = await this.folderService.encrypt(importResult.folders[i]);
request.folders.push(new FolderRequest(f));
}
importer.organizationId = organizationId;
return importer;
}
private getImporterInstance(format: string) {
if (format == null || format === '') {
return null;
}
if (importResult.folderRelationships != null) {
importResult.folderRelationships.forEach((r) =>
request.folderRelationships.push(new KvpRequest(r[0], r[1]))
);
}
return await this.apiService.postImportCiphers(request);
} else {
const request = new ImportOrganizationCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
importResult.ciphers[i].organizationId = organizationId;
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
request.ciphers.push(new CipherRequest(c));
}
if (importResult.collections != null) {
for (let i = 0; i < importResult.collections.length; i++) {
importResult.collections[i].organizationId = organizationId;
const c = await this.collectionService.encrypt(importResult.collections[i]);
request.collections.push(new CollectionRequest(c));
}
}
if (importResult.collectionRelationships != null) {
importResult.collectionRelationships.forEach((r) =>
request.collectionRelationships.push(new KvpRequest(r[0], r[1]))
);
}
return await this.apiService.postImportOrganizationCiphers(organizationId, request);
}
}
switch (format) {
case 'bitwardencsv':
return new BitwardenCsvImporter();
case 'bitwardenjson':
return new BitwardenJsonImporter(this.cryptoService, this.i18nService);
case 'lastpasscsv':
case 'passboltcsv':
return new LastPassCsvImporter();
case 'keepassxcsv':
return new KeePassXCsvImporter();
case 'aviracsv':
return new AviraCsvImporter();
case 'blurcsv':
return new BlurCsvImporter();
case 'safeincloudxml':
return new SafeInCloudXmlImporter();
case 'padlockcsv':
return new PadlockCsvImporter();
case 'keepass2xml':
return new KeePass2XmlImporter();
case 'chromecsv':
case 'operacsv':
case 'vivaldicsv':
return new ChromeCsvImporter();
case 'firefoxcsv':
return new FirefoxCsvImporter();
case 'upmcsv':
return new UpmCsvImporter();
case 'saferpasscsv':
return new SaferPassCsvImporter();
case 'safaricsv':
return new SafariCsvImporter();
case 'meldiumcsv':
return new MeldiumCsvImporter();
case '1password1pif':
return new OnePassword1PifImporter();
case '1passwordwincsv':
return new OnePasswordWinCsvImporter();
case '1passwordmaccsv':
return new OnePasswordMacCsvImporter();
case 'keepercsv':
return new KeeperCsvImporter();
case 'passworddragonxml':
return new PasswordDragonXmlImporter();
case 'enpasscsv':
return new EnpassCsvImporter();
case 'enpassjson':
return new EnpassJsonImporter();
case 'pwsafexml':
return new PasswordSafeXmlImporter();
case 'dashlanejson':
return new DashlaneJsonImporter();
case 'msecurecsv':
return new MSecureCsvImporter();
case 'stickypasswordxml':
return new StickyPasswordXmlImporter();
case 'truekeycsv':
return new TrueKeyCsvImporter();
case 'clipperzhtml':
return new ClipperzHtmlImporter();
case 'roboformcsv':
return new RoboFormCsvImporter();
case 'ascendocsv':
return new AscendoCsvImporter();
case 'passwordbossjson':
return new PasswordBossJsonImporter();
case 'zohovaultcsv':
return new ZohoVaultCsvImporter();
case 'splashidcsv':
return new SplashIdCsvImporter();
case 'passkeepcsv':
return new PassKeepCsvImporter();
case 'gnomejson':
return new GnomeJsonImporter();
case 'passwordagentcsv':
return new PasswordAgentCsvImporter();
case 'passpackcsv':
return new PasspackCsvImporter();
case 'passmanjson':
return new PassmanJsonImporter();
case 'avastcsv':
return new AvastCsvImporter();
case 'avastjson':
return new AvastJsonImporter();
case 'fsecurefsk':
return new FSecureFskImporter();
case 'kasperskytxt':
return new KasperskyTxtImporter();
case 'remembearcsv':
return new RememBearCsvImporter();
case 'passwordwallettxt':
return new PasswordWalletTxtImporter();
case 'mykicsv':
return new MykiCsvImporter();
case 'securesafecsv':
return new SecureSafeCsvImporter();
case 'logmeoncecsv':
return new LogMeOnceCsvImporter();
case 'blackberrycsv':
return new BlackBerryCsvImporter();
case 'buttercupcsv':
return new ButtercupCsvImporter();
case 'codebookcsv':
return new CodebookCsvImporter();
case 'encryptrcsv':
return new EncryptrCsvImporter();
case 'yoticsv':
return new YotiCsvImporter();
case 'nordpasscsv':
return new NordPassCsvImporter();
default:
return null;
}
private badData(c: CipherView) {
return (
(c.name == null || c.name === "--") &&
c.type === CipherType.Login &&
c.login != null &&
Utils.isNullOrWhitespace(c.login.password)
);
}
private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): Error {
if (errorResponse.validationErrors == null) {
return new Error(errorResponse.message);
}
private async postImport(importResult: ImportResult, organizationId: string = null) {
if (organizationId == null) {
const request = new ImportCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
request.ciphers.push(new CipherRequest(c));
}
if (importResult.folders != null) {
for (let i = 0; i < importResult.folders.length; i++) {
const f = await this.folderService.encrypt(importResult.folders[i]);
request.folders.push(new FolderRequest(f));
}
}
if (importResult.folderRelationships != null) {
importResult.folderRelationships.forEach(r =>
request.folderRelationships.push(new KvpRequest(r[0], r[1])));
}
return await this.apiService.postImportCiphers(request);
} else {
const request = new ImportOrganizationCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
importResult.ciphers[i].organizationId = organizationId;
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
request.ciphers.push(new CipherRequest(c));
}
if (importResult.collections != null) {
for (let i = 0; i < importResult.collections.length; i++) {
importResult.collections[i].organizationId = organizationId;
const c = await this.collectionService.encrypt(importResult.collections[i]);
request.collections.push(new CollectionRequest(c));
}
}
if (importResult.collectionRelationships != null) {
importResult.collectionRelationships.forEach(r =>
request.collectionRelationships.push(new KvpRequest(r[0], r[1])));
}
return await this.apiService.postImportOrganizationCiphers(organizationId, request);
}
}
let errorMessage = "";
private badData(c: CipherView) {
return (c.name == null || c.name === '--') &&
(c.type === CipherType.Login && c.login != null && Utils.isNullOrWhitespace(c.login.password));
}
Object.entries(errorResponse.validationErrors).forEach(([key, value], index) => {
let item;
let itemType;
const i = Number(key.match(/[0-9]+/)[0]);
private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): Error {
if (errorResponse.validationErrors == null) {
return new Error(errorResponse.message);
}
switch (key.match(/^\w+/)[0]) {
case "Ciphers":
item = importResult.ciphers[i];
itemType = CipherType[item.type];
break;
case "Folders":
item = importResult.folders[i];
itemType = "Folder";
break;
case "Collections":
item = importResult.collections[i];
itemType = "Collection";
break;
default:
return;
}
let errorMessage = '';
if (index > 0) {
errorMessage += "\n\n";
}
Object.entries(errorResponse.validationErrors).forEach(([key, value], index) => {
let item;
let itemType;
const i = Number(key.match(/[0-9]+/)[0]);
if (itemType !== "Folder" && itemType !== "Collection") {
errorMessage += "[" + (i + 1) + "] ";
}
switch (key.match(/^\w+/)[0]) {
case 'Ciphers':
item = importResult.ciphers[i];
itemType = CipherType[item.type];
break;
case 'Folders':
item = importResult.folders[i];
itemType = 'Folder';
break;
case 'Collections':
item = importResult.collections[i];
itemType = 'Collection';
break;
default:
return;
}
errorMessage += "[" + itemType + '] "' + item.name + '": ' + value;
});
if (index > 0) {
errorMessage += '\n\n';
}
if (itemType !== 'Folder' && itemType !== 'Collection') {
errorMessage += '[' + (i + 1) + '] ';
}
errorMessage += '[' + itemType + '] "' + item.name + '": ' + value;
});
return new Error(errorMessage);
}
return new Error(errorMessage);
}
}

View File

@@ -1,88 +1,98 @@
import { ApiService } from '../abstractions/api.service';
import { CryptoService } from '../abstractions/crypto.service';
import { KeyConnectorService as KeyConnectorServiceAbstraction } from '../abstractions/keyConnector.service';
import { LogService } from '../abstractions/log.service';
import { OrganizationService } from '../abstractions/organization.service';
import { StateService } from '../abstractions/state.service';
import { TokenService } from '../abstractions/token.service';
import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization.service";
import { StateService } from "../abstractions/state.service";
import { TokenService } from "../abstractions/token.service";
import { OrganizationUserType } from '../enums/organizationUserType';
import { OrganizationUserType } from "../enums/organizationUserType";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest';
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
constructor(private stateService: StateService, private cryptoService: CryptoService,
private apiService: ApiService, private tokenService: TokenService,
private logService: LogService, private organizationService: OrganizationService) { }
constructor(
private stateService: StateService,
private cryptoService: CryptoService,
private apiService: ApiService,
private tokenService: TokenService,
private logService: LogService,
private organizationService: OrganizationService
) {}
setUsesKeyConnector(usesKeyConnector: boolean) {
return this.stateService.setUsesKeyConnector(usesKeyConnector);
setUsesKeyConnector(usesKeyConnector: boolean) {
return this.stateService.setUsesKeyConnector(usesKeyConnector);
}
async getUsesKeyConnector(): Promise<boolean> {
return await this.stateService.getUsesKeyConnector();
}
async userNeedsMigration() {
const loggedInUsingSso = this.tokenService.getIsExternal();
const requiredByOrganization = (await this.getManagingOrganization()) != null;
const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector());
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
}
async migrateUser() {
const organization = await this.getManagingOrganization();
const key = await this.cryptoService.getKey();
try {
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
await this.apiService.postUserKeyToKeyConnector(
organization.keyConnectorUrl,
keyConnectorRequest
);
} catch (e) {
throw new Error("Unable to reach key connector");
}
async getUsesKeyConnector(): Promise<boolean> {
return await this.stateService.getUsesKeyConnector();
await this.apiService.postConvertToKeyConnector();
}
async getAndSetKey(url: string) {
try {
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
const k = new SymmetricCryptoKey(keyArr);
await this.cryptoService.setKey(k);
} catch (e) {
this.logService.error(e);
throw new Error("Unable to reach key connector");
}
}
async userNeedsMigration() {
const loggedInUsingSso = this.tokenService.getIsExternal();
const requiredByOrganization = await this.getManagingOrganization() != null;
const userIsNotUsingKeyConnector = !await this.getUsesKeyConnector();
async getManagingOrganization() {
const orgs = await this.organizationService.getAll();
return orgs.find(
(o) =>
o.keyConnectorEnabled &&
o.type !== OrganizationUserType.Admin &&
o.type !== OrganizationUserType.Owner &&
!o.isProviderUser
);
}
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
}
async setConvertAccountRequired(status: boolean) {
await this.stateService.setConvertAccountToKeyConnector(status);
}
async migrateUser() {
const organization = await this.getManagingOrganization();
const key = await this.cryptoService.getKey();
async getConvertAccountRequired(): Promise<boolean> {
return await this.stateService.getConvertAccountToKeyConnector();
}
try {
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
await this.apiService.postUserKeyToKeyConnector(organization.keyConnectorUrl, keyConnectorRequest);
} catch (e) {
throw new Error('Unable to reach key connector');
}
async removeConvertAccountRequired() {
await this.stateService.setConvertAccountToKeyConnector(null);
}
await this.apiService.postConvertToKeyConnector();
}
async getAndSetKey(url: string) {
try {
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
const k = new SymmetricCryptoKey(keyArr);
await this.cryptoService.setKey(k);
} catch (e) {
this.logService.error(e);
throw new Error('Unable to reach key connector');
}
}
async getManagingOrganization() {
const orgs = await this.organizationService.getAll();
return orgs.find(o =>
o.keyConnectorEnabled &&
o.type !== OrganizationUserType.Admin &&
o.type !== OrganizationUserType.Owner &&
!o.isProviderUser);
}
async setConvertAccountRequired(status: boolean) {
await this.stateService.setConvertAccountToKeyConnector(status);
}
async getConvertAccountRequired(): Promise<boolean> {
return await this.stateService.getConvertAccountToKeyConnector();
}
async removeConvertAccountRequired() {
await this.stateService.setConvertAccountToKeyConnector(null);
}
async clear() {
await this.removeConvertAccountRequired();
}
async clear() {
await this.removeConvertAccountRequired();
}
}

View File

@@ -1,7 +1,7 @@
import { MessagingService } from '../abstractions/messaging.service';
import { MessagingService } from "../abstractions/messaging.service";
export class NoopMessagingService implements MessagingService {
send(subscriber: string, arg: any = {}) {
// Do nothing...
}
send(subscriber: string, arg: any = {}) {
// Do nothing...
}
}

View File

@@ -1,217 +1,231 @@
import * as signalR from '@microsoft/signalr';
import * as signalRMsgPack from '@microsoft/signalr-protocol-msgpack';
import * as signalR from "@microsoft/signalr";
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
import { NotificationType } from '../enums/notificationType';
import { NotificationType } from "../enums/notificationType";
import { ApiService } from '../abstractions/api.service';
import { AppIdService } from '../abstractions/appId.service';
import { EnvironmentService } from '../abstractions/environment.service';
import { LogService } from '../abstractions/log.service';
import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service';
import { StateService } from '../abstractions/state.service';
import { SyncService } from '../abstractions/sync.service';
import { VaultTimeoutService } from '../abstractions/vaultTimeout.service';
import { ApiService } from "../abstractions/api.service";
import { AppIdService } from "../abstractions/appId.service";
import { EnvironmentService } from "../abstractions/environment.service";
import { LogService } from "../abstractions/log.service";
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
import { StateService } from "../abstractions/state.service";
import { SyncService } from "../abstractions/sync.service";
import { VaultTimeoutService } from "../abstractions/vaultTimeout.service";
import {
NotificationResponse,
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
} from '../models/response/notificationResponse';
NotificationResponse,
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
} from "../models/response/notificationResponse";
export class NotificationsService implements NotificationsServiceAbstraction {
private signalrConnection: signalR.HubConnection;
private url: string;
private connected = false;
private inited = false;
private inactive = false;
private reconnectTimer: any = null;
private signalrConnection: signalR.HubConnection;
private url: string;
private connected = false;
private inited = false;
private inactive = false;
private reconnectTimer: any = null;
constructor(private syncService: SyncService, private appIdService: AppIdService,
private apiService: ApiService, private vaultTimeoutService: VaultTimeoutService,
private environmentService: EnvironmentService, private logoutCallback: () => Promise<void>,
private logService: LogService, private stateService: StateService) {
this.environmentService.urls.subscribe(() => {
if (!this.inited) {
return;
}
constructor(
private syncService: SyncService,
private appIdService: AppIdService,
private apiService: ApiService,
private vaultTimeoutService: VaultTimeoutService,
private environmentService: EnvironmentService,
private logoutCallback: () => Promise<void>,
private logService: LogService,
private stateService: StateService
) {
this.environmentService.urls.subscribe(() => {
if (!this.inited) {
return;
}
this.init();
});
this.init();
});
}
async init(): Promise<void> {
this.inited = false;
this.url = this.environmentService.getNotificationsUrl();
// Set notifications server URL to `https://-` to effectively disable communication
// with the notifications server from the client app
if (this.url === "https://-") {
return;
}
async init(): Promise<void> {
this.inited = false;
this.url = this.environmentService.getNotificationsUrl();
// Set notifications server URL to `https://-` to effectively disable communication
// with the notifications server from the client app
if (this.url === 'https://-') {
return;
}
if (this.signalrConnection != null) {
this.signalrConnection.off('ReceiveMessage');
this.signalrConnection.off('Heartbeat');
await this.signalrConnection.stop();
this.connected = false;
this.signalrConnection = null;
}
this.signalrConnection = new signalR.HubConnectionBuilder()
.withUrl(this.url + '/hub', {
accessTokenFactory: () => this.apiService.getActiveBearerToken(),
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol)
// .configureLogging(signalR.LogLevel.Trace)
.build();
this.signalrConnection.on('ReceiveMessage',
(data: any) => this.processNotification(new NotificationResponse(data)));
this.signalrConnection.on('Heartbeat',
(data: any) => { /*console.log('Heartbeat!');*/ });
this.signalrConnection.onclose(() => {
this.connected = false;
this.reconnect(true);
});
this.inited = true;
if (await this.isAuthedAndUnlocked()) {
await this.reconnect(false);
}
if (this.signalrConnection != null) {
this.signalrConnection.off("ReceiveMessage");
this.signalrConnection.off("Heartbeat");
await this.signalrConnection.stop();
this.connected = false;
this.signalrConnection = null;
}
async updateConnection(sync = false): Promise<void> {
if (!this.inited) {
return;
}
try {
if (await this.isAuthedAndUnlocked()) {
await this.reconnect(sync);
} else {
await this.signalrConnection.stop();
}
} catch (e) {
this.logService.error(e.toString());
}
this.signalrConnection = new signalR.HubConnectionBuilder()
.withUrl(this.url + "/hub", {
accessTokenFactory: () => this.apiService.getActiveBearerToken(),
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol)
// .configureLogging(signalR.LogLevel.Trace)
.build();
this.signalrConnection.on("ReceiveMessage", (data: any) =>
this.processNotification(new NotificationResponse(data))
);
this.signalrConnection.on("Heartbeat", (data: any) => {
/*console.log('Heartbeat!');*/
});
this.signalrConnection.onclose(() => {
this.connected = false;
this.reconnect(true);
});
this.inited = true;
if (await this.isAuthedAndUnlocked()) {
await this.reconnect(false);
}
}
async updateConnection(sync = false): Promise<void> {
if (!this.inited) {
return;
}
try {
if (await this.isAuthedAndUnlocked()) {
await this.reconnect(sync);
} else {
await this.signalrConnection.stop();
}
} catch (e) {
this.logService.error(e.toString());
}
}
async reconnectFromActivity(): Promise<void> {
this.inactive = false;
if (this.inited && !this.connected) {
await this.reconnect(true);
}
}
async disconnectFromInactivity(): Promise<void> {
this.inactive = true;
if (this.inited && this.connected) {
await this.signalrConnection.stop();
}
}
private async processNotification(notification: NotificationResponse) {
const appId = await this.appIdService.getAppId();
if (notification == null || notification.contextId === appId) {
return;
}
async reconnectFromActivity(): Promise<void> {
this.inactive = false;
if (this.inited && !this.connected) {
await this.reconnect(true);
}
const isAuthenticated = await this.stateService.getIsAuthenticated();
const payloadUserId = notification.payload.userId || notification.payload.UserId;
const myUserId = await this.stateService.getUserId();
if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) {
return;
}
async disconnectFromInactivity(): Promise<void> {
this.inactive = true;
if (this.inited && this.connected) {
await this.signalrConnection.stop();
switch (notification.type) {
case NotificationType.SyncCipherCreate:
case NotificationType.SyncCipherUpdate:
await this.syncService.syncUpsertCipher(
notification.payload as SyncCipherNotification,
notification.type === NotificationType.SyncCipherUpdate
);
break;
case NotificationType.SyncCipherDelete:
case NotificationType.SyncLoginDelete:
await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification);
break;
case NotificationType.SyncFolderCreate:
case NotificationType.SyncFolderUpdate:
await this.syncService.syncUpsertFolder(
notification.payload as SyncFolderNotification,
notification.type === NotificationType.SyncFolderUpdate
);
break;
case NotificationType.SyncFolderDelete:
await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification);
break;
case NotificationType.SyncVault:
case NotificationType.SyncCiphers:
case NotificationType.SyncSettings:
if (isAuthenticated) {
await this.syncService.fullSync(false);
}
break;
case NotificationType.SyncOrgKeys:
if (isAuthenticated) {
await this.syncService.fullSync(true);
// Stop so a reconnect can be made
await this.signalrConnection.stop();
}
break;
case NotificationType.LogOut:
if (isAuthenticated) {
this.logoutCallback();
}
break;
case NotificationType.SyncSendCreate:
case NotificationType.SyncSendUpdate:
await this.syncService.syncUpsertSend(
notification.payload as SyncSendNotification,
notification.type === NotificationType.SyncSendUpdate
);
break;
case NotificationType.SyncSendDelete:
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
default:
break;
}
}
private async reconnect(sync: boolean) {
if (this.reconnectTimer != null) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.connected || !this.inited || this.inactive) {
return;
}
const authedAndUnlocked = await this.isAuthedAndUnlocked();
if (!authedAndUnlocked) {
return;
}
private async processNotification(notification: NotificationResponse) {
const appId = await this.appIdService.getAppId();
if (notification == null || notification.contextId === appId) {
return;
}
const isAuthenticated = await this.stateService.getIsAuthenticated();
const payloadUserId = notification.payload.userId || notification.payload.UserId;
const myUserId = await this.stateService.getUserId();
if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) {
return;
}
switch (notification.type) {
case NotificationType.SyncCipherCreate:
case NotificationType.SyncCipherUpdate:
await this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification,
notification.type === NotificationType.SyncCipherUpdate);
break;
case NotificationType.SyncCipherDelete:
case NotificationType.SyncLoginDelete:
await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification);
break;
case NotificationType.SyncFolderCreate:
case NotificationType.SyncFolderUpdate:
await this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification,
notification.type === NotificationType.SyncFolderUpdate);
break;
case NotificationType.SyncFolderDelete:
await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification);
break;
case NotificationType.SyncVault:
case NotificationType.SyncCiphers:
case NotificationType.SyncSettings:
if (isAuthenticated) {
await this.syncService.fullSync(false);
}
break;
case NotificationType.SyncOrgKeys:
if (isAuthenticated) {
await this.syncService.fullSync(true);
// Stop so a reconnect can be made
await this.signalrConnection.stop();
}
break;
case NotificationType.LogOut:
if (isAuthenticated) {
this.logoutCallback();
}
break;
case NotificationType.SyncSendCreate:
case NotificationType.SyncSendUpdate:
await this.syncService.syncUpsertSend(notification.payload as SyncSendNotification,
notification.type === NotificationType.SyncSendUpdate);
break;
case NotificationType.SyncSendDelete:
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
default:
break;
}
try {
await this.signalrConnection.start();
this.connected = true;
if (sync) {
await this.syncService.fullSync(false);
}
} catch (e) {
this.logService.error(e);
}
private async reconnect(sync: boolean) {
if (this.reconnectTimer != null) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.connected || !this.inited || this.inactive) {
return;
}
const authedAndUnlocked = await this.isAuthedAndUnlocked();
if (!authedAndUnlocked) {
return;
}
try {
await this.signalrConnection.start();
this.connected = true;
if (sync) {
await this.syncService.fullSync(false);
}
} catch (e) {
this.logService.error(e);
}
if (!this.connected) {
this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000));
}
if (!this.connected) {
this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000));
}
}
private async isAuthedAndUnlocked() {
if (await this.stateService.getIsAuthenticated()) {
const locked = await this.vaultTimeoutService.isLocked();
return !locked;
}
return false;
private async isAuthedAndUnlocked() {
if (await this.stateService.getIsAuthenticated()) {
const locked = await this.vaultTimeoutService.isLocked();
return !locked;
}
return false;
}
private random(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
private random(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}

View File

@@ -1,49 +1,50 @@
import { OrganizationService as OrganizationServiceAbstraction } from '../abstractions/organization.service';
import { StateService } from '../abstractions/state.service';
import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service";
import { StateService } from "../abstractions/state.service";
import { OrganizationData } from '../models/data/organizationData';
import { OrganizationData } from "../models/data/organizationData";
import { Organization } from '../models/domain/organization';
import { Organization } from "../models/domain/organization";
export class OrganizationService implements OrganizationServiceAbstraction {
constructor(private stateService: StateService) {
constructor(private stateService: StateService) {}
async get(id: string): Promise<Organization> {
const organizations = await this.stateService.getOrganizations();
if (organizations == null || !organizations.hasOwnProperty(id)) {
return null;
}
async get(id: string): Promise<Organization> {
const organizations = await this.stateService.getOrganizations();
if (organizations == null || !organizations.hasOwnProperty(id)) {
return null;
}
return new Organization(organizations[id]);
}
return new Organization(organizations[id]);
async getByIdentifier(identifier: string): Promise<Organization> {
const organizations = await this.getAll();
if (organizations == null || organizations.length === 0) {
return null;
}
async getByIdentifier(identifier: string): Promise<Organization> {
const organizations = await this.getAll();
if (organizations == null || organizations.length === 0) {
return null;
}
return organizations.find((o) => o.identifier === identifier);
}
return organizations.find(o => o.identifier === identifier);
async getAll(userId?: string): Promise<Organization[]> {
const organizations = await this.stateService.getOrganizations({ userId: userId });
const response: Organization[] = [];
for (const id in organizations) {
if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) {
response.push(new Organization(organizations[id]));
}
}
return response;
}
async getAll(userId?: string): Promise<Organization[]> {
const organizations = await this.stateService.getOrganizations({ userId: userId });
const response: Organization[] = [];
for (const id in organizations) {
if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) {
response.push(new Organization(organizations[id]));
}
}
return response;
}
async save(organizations: { [id: string]: OrganizationData }) {
return await this.stateService.setOrganizations(organizations);
}
async save(organizations: {[id: string]: OrganizationData}) {
return await this.stateService.setOrganizations(organizations);
}
async canManageSponsorships(): Promise<boolean> {
const orgs = await this.getAll();
return orgs.some(o => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null);
}
async canManageSponsorships(): Promise<boolean> {
const orgs = await this.getAll();
return orgs.some(
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,215 +1,240 @@
import { OrganizationService } from '../abstractions/organization.service';
import { PolicyService as PolicyServiceAbstraction } from '../abstractions/policy.service';
import { StateService } from '../abstractions/state.service';
import { OrganizationService } from "../abstractions/organization.service";
import { PolicyService as PolicyServiceAbstraction } from "../abstractions/policy.service";
import { StateService } from "../abstractions/state.service";
import { PolicyData } from '../models/data/policyData';
import { PolicyData } from "../models/data/policyData";
import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions';
import { Organization } from '../models/domain/organization';
import { Policy } from '../models/domain/policy';
import { ResetPasswordPolicyOptions } from '../models/domain/resetPasswordPolicyOptions';
import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions";
import { Organization } from "../models/domain/organization";
import { Policy } from "../models/domain/policy";
import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions";
import { OrganizationUserStatusType } from '../enums/organizationUserStatusType';
import { OrganizationUserType } from '../enums/organizationUserType';
import { PolicyType } from '../enums/policyType';
import { OrganizationUserStatusType } from "../enums/organizationUserStatusType";
import { OrganizationUserType } from "../enums/organizationUserType";
import { PolicyType } from "../enums/policyType";
import { ApiService } from '../abstractions/api.service';
import { ListResponse } from '../models/response/listResponse';
import { PolicyResponse } from '../models/response/policyResponse';
import { ApiService } from "../abstractions/api.service";
import { ListResponse } from "../models/response/listResponse";
import { PolicyResponse } from "../models/response/policyResponse";
export class PolicyService implements PolicyServiceAbstraction {
policyCache: Policy[];
policyCache: Policy[];
constructor(private stateService: StateService, private organizationService: OrganizationService,
private apiService: ApiService) {
constructor(
private stateService: StateService,
private organizationService: OrganizationService,
private apiService: ApiService
) {}
async clearCache(): Promise<void> {
await this.stateService.setDecryptedPolicies(null);
}
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
let response: Policy[] = [];
const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId });
if (decryptedPolicies != null) {
response = decryptedPolicies;
} else {
const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId });
for (const id in diskPolicies) {
if (diskPolicies.hasOwnProperty(id)) {
response.push(new Policy(diskPolicies[id]));
}
}
await this.stateService.setDecryptedPolicies(response, { userId: userId });
}
if (type != null) {
return response.filter((policy) => policy.type === type);
} else {
return response;
}
}
async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise<Policy> {
const org = await this.organizationService.get(organizationId);
if (org?.isProviderUser) {
const orgPolicies = await this.apiService.getPolicies(organizationId);
const policy = orgPolicies.data.find((p) => p.organizationId === organizationId);
if (policy == null) {
return null;
}
return new Policy(new PolicyData(policy));
}
async clearCache(): Promise<void> {
await this.stateService.setDecryptedPolicies(null);
const policies = await this.getAll(policyType);
return policies.find((p) => p.organizationId === organizationId);
}
async replace(policies: { [id: string]: PolicyData }): Promise<any> {
await this.stateService.setDecryptedPolicies(null);
await this.stateService.setEncryptedPolicies(policies);
}
async clear(userId?: string): Promise<any> {
await this.stateService.setDecryptedPolicies(null, { userId: userId });
await this.stateService.setEncryptedPolicies(null, { userId: userId });
}
async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise<MasterPasswordPolicyOptions> {
let enforcedOptions: MasterPasswordPolicyOptions = null;
if (policies == null) {
policies = await this.getAll(PolicyType.MasterPassword);
} else {
policies = policies.filter((p) => p.type === PolicyType.MasterPassword);
}
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
let response: Policy[] = [];
const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId });
if (decryptedPolicies != null) {
response = decryptedPolicies;
} else {
const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId });
for (const id in diskPolicies) {
if (diskPolicies.hasOwnProperty(id)) {
response.push(new Policy(diskPolicies[id]));
}
}
await this.stateService.setDecryptedPolicies(response, { userId: userId });
}
if (type != null) {
return response.filter(policy => policy.type === type);
} else {
return response;
}
if (policies == null || policies.length === 0) {
return enforcedOptions;
}
async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise<Policy> {
const org = await this.organizationService.get(organizationId);
if (org?.isProviderUser) {
const orgPolicies = await this.apiService.getPolicies(organizationId);
const policy = orgPolicies.data.find(p => p.organizationId === organizationId);
policies.forEach((currentPolicy) => {
if (!currentPolicy.enabled || currentPolicy.data == null) {
return;
}
if (policy == null) {
return null;
}
if (enforcedOptions == null) {
enforcedOptions = new MasterPasswordPolicyOptions();
}
return new Policy(new PolicyData(policy));
}
if (
currentPolicy.data.minComplexity != null &&
currentPolicy.data.minComplexity > enforcedOptions.minComplexity
) {
enforcedOptions.minComplexity = currentPolicy.data.minComplexity;
}
const policies = await this.getAll(policyType);
return policies.find(p => p.organizationId === organizationId);
if (
currentPolicy.data.minLength != null &&
currentPolicy.data.minLength > enforcedOptions.minLength
) {
enforcedOptions.minLength = currentPolicy.data.minLength;
}
if (currentPolicy.data.requireUpper) {
enforcedOptions.requireUpper = true;
}
if (currentPolicy.data.requireLower) {
enforcedOptions.requireLower = true;
}
if (currentPolicy.data.requireNumbers) {
enforcedOptions.requireNumbers = true;
}
if (currentPolicy.data.requireSpecial) {
enforcedOptions.requireSpecial = true;
}
});
return enforcedOptions;
}
evaluateMasterPassword(
passwordStrength: number,
newPassword: string,
enforcedPolicyOptions: MasterPasswordPolicyOptions
): boolean {
if (enforcedPolicyOptions == null) {
return true;
}
async replace(policies: { [id: string]: PolicyData; }): Promise<any> {
await this.stateService.setDecryptedPolicies(null);
await this.stateService.setEncryptedPolicies(policies);
if (
enforcedPolicyOptions.minComplexity > 0 &&
enforcedPolicyOptions.minComplexity > passwordStrength
) {
return false;
}
async clear(userId?: string): Promise<any> {
await this.stateService.setDecryptedPolicies(null, { userId: userId });
await this.stateService.setEncryptedPolicies(null, { userId: userId });
if (
enforcedPolicyOptions.minLength > 0 &&
enforcedPolicyOptions.minLength > newPassword.length
) {
return false;
}
async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise<MasterPasswordPolicyOptions> {
let enforcedOptions: MasterPasswordPolicyOptions = null;
if (policies == null) {
policies = await this.getAll(PolicyType.MasterPassword);
} else {
policies = policies.filter(p => p.type === PolicyType.MasterPassword);
}
if (policies == null || policies.length === 0) {
return enforcedOptions;
}
policies.forEach(currentPolicy => {
if (!currentPolicy.enabled || currentPolicy.data == null) {
return;
}
if (enforcedOptions == null) {
enforcedOptions = new MasterPasswordPolicyOptions();
}
if (currentPolicy.data.minComplexity != null
&& currentPolicy.data.minComplexity > enforcedOptions.minComplexity) {
enforcedOptions.minComplexity = currentPolicy.data.minComplexity;
}
if (currentPolicy.data.minLength != null
&& currentPolicy.data.minLength > enforcedOptions.minLength) {
enforcedOptions.minLength = currentPolicy.data.minLength;
}
if (currentPolicy.data.requireUpper) {
enforcedOptions.requireUpper = true;
}
if (currentPolicy.data.requireLower) {
enforcedOptions.requireLower = true;
}
if (currentPolicy.data.requireNumbers) {
enforcedOptions.requireNumbers = true;
}
if (currentPolicy.data.requireSpecial) {
enforcedOptions.requireSpecial = true;
}
});
return enforcedOptions;
if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) {
return false;
}
evaluateMasterPassword(passwordStrength: number, newPassword: string,
enforcedPolicyOptions: MasterPasswordPolicyOptions): boolean {
if (enforcedPolicyOptions == null) {
return true;
}
if (enforcedPolicyOptions.minComplexity > 0 && enforcedPolicyOptions.minComplexity > passwordStrength) {
return false;
}
if (enforcedPolicyOptions.minLength > 0 && enforcedPolicyOptions.minLength > newPassword.length) {
return false;
}
if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) {
return false;
}
if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) {
return false;
}
if (enforcedPolicyOptions.requireNumbers && !(/[0-9]/.test(newPassword))) {
return false;
}
if (enforcedPolicyOptions.requireSpecial && !(/[!@#$%\^&*]/g.test(newPassword))) {
return false;
}
return true;
if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) {
return false;
}
getResetPasswordPolicyOptions(policies: Policy[], orgId: string): [ResetPasswordPolicyOptions, boolean] {
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
if (policies == null || orgId == null) {
return [resetPasswordPolicyOptions, false];
}
const policy = policies.find(p => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled);
resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false;
return [resetPasswordPolicyOptions, policy?.enabled ?? false];
if (enforcedPolicyOptions.requireNumbers && !/[0-9]/.test(newPassword)) {
return false;
}
mapPoliciesFromToken(policiesResponse: ListResponse<PolicyResponse>): Policy[] {
if (policiesResponse == null || policiesResponse.data == null) {
return null;
}
const policiesData = policiesResponse.data.map(p => new PolicyData(p));
return policiesData.map(p => new Policy(p));
if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) {
return false;
}
async policyAppliesToUser(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean, userId?: string) {
const policies = await this.getAll(policyType, userId);
const organizations = await this.organizationService.getAll(userId);
let filteredPolicies;
return true;
}
if (policyFilter != null) {
filteredPolicies = policies.filter(p => p.enabled && policyFilter(p));
}
else {
filteredPolicies = policies.filter(p => p.enabled);
}
getResetPasswordPolicyOptions(
policies: Policy[],
orgId: string
): [ResetPasswordPolicyOptions, boolean] {
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
const policySet = new Set(filteredPolicies.map(p => p.organizationId));
return organizations.some(o =>
o.enabled &&
o.status >= OrganizationUserStatusType.Accepted &&
o.usePolicies &&
!this.isExcemptFromPolicies(o, policyType) &&
policySet.has(o.id));
if (policies == null || orgId == null) {
return [resetPasswordPolicyOptions, false];
}
private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) {
if (policyType === PolicyType.MaximumVaultTimeout) {
return organization.type === OrganizationUserType.Owner;
}
const policy = policies.find(
(p) => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled
);
resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false;
return organization.isExemptFromPolicies;
return [resetPasswordPolicyOptions, policy?.enabled ?? false];
}
mapPoliciesFromToken(policiesResponse: ListResponse<PolicyResponse>): Policy[] {
if (policiesResponse == null || policiesResponse.data == null) {
return null;
}
const policiesData = policiesResponse.data.map((p) => new PolicyData(p));
return policiesData.map((p) => new Policy(p));
}
async policyAppliesToUser(
policyType: PolicyType,
policyFilter?: (policy: Policy) => boolean,
userId?: string
) {
const policies = await this.getAll(policyType, userId);
const organizations = await this.organizationService.getAll(userId);
let filteredPolicies;
if (policyFilter != null) {
filteredPolicies = policies.filter((p) => p.enabled && policyFilter(p));
} else {
filteredPolicies = policies.filter((p) => p.enabled);
}
const policySet = new Set(filteredPolicies.map((p) => p.organizationId));
return organizations.some(
(o) =>
o.enabled &&
o.status >= OrganizationUserStatusType.Accepted &&
o.usePolicies &&
!this.isExcemptFromPolicies(o, policyType) &&
policySet.has(o.id)
);
}
private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) {
if (policyType === PolicyType.MaximumVaultTimeout) {
return organization.type === OrganizationUserType.Owner;
}
return organization.isExemptFromPolicies;
}
}

View File

@@ -1,35 +1,34 @@
import { ProviderService as ProviderServiceAbstraction } from '../abstractions/provider.service';
import { StateService } from '../abstractions/state.service';
import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service";
import { StateService } from "../abstractions/state.service";
import { ProviderData } from '../models/data/providerData';
import { ProviderData } from "../models/data/providerData";
import { Provider } from '../models/domain/provider';
import { Provider } from "../models/domain/provider";
export class ProviderService implements ProviderServiceAbstraction {
constructor(private stateService: StateService) {
constructor(private stateService: StateService) {}
async get(id: string): Promise<Provider> {
const providers = await this.stateService.getProviders();
if (providers == null || !providers.hasOwnProperty(id)) {
return null;
}
async get(id: string): Promise<Provider> {
const providers = await this.stateService.getProviders();
if (providers == null || !providers.hasOwnProperty(id)) {
return null;
}
return new Provider(providers[id]);
}
return new Provider(providers[id]);
async getAll(): Promise<Provider[]> {
const providers = await this.stateService.getProviders();
const response: Provider[] = [];
for (const id in providers) {
if (providers.hasOwnProperty(id)) {
response.push(new Provider(providers[id]));
}
}
return response;
}
async getAll(): Promise<Provider[]> {
const providers = await this.stateService.getProviders();
const response: Provider[] = [];
for (const id in providers) {
if (providers.hasOwnProperty(id)) {
response.push(new Provider(providers[id]));
}
}
return response;
}
async save(providers: { [id: string]: ProviderData; }) {
await this.stateService.setProviders(providers);
}
async save(providers: { [id: string]: ProviderData }) {
await this.stateService.setProviders(providers);
}
}

View File

@@ -1,272 +1,287 @@
import * as lunr from 'lunr';
import * as lunr from "lunr";
import { CipherView } from '../models/view/cipherView';
import { CipherView } from "../models/view/cipherView";
import { CipherService } from '../abstractions/cipher.service';
import { I18nService } from '../abstractions/i18n.service';
import { LogService } from '../abstractions/log.service';
import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service';
import { CipherService } from "../abstractions/cipher.service";
import { I18nService } from "../abstractions/i18n.service";
import { LogService } from "../abstractions/log.service";
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
import { CipherType } from '../enums/cipherType';
import { FieldType } from '../enums/fieldType';
import { UriMatchType } from '../enums/uriMatchType';
import { SendView } from '../models/view/sendView';
import { CipherType } from "../enums/cipherType";
import { FieldType } from "../enums/fieldType";
import { UriMatchType } from "../enums/uriMatchType";
import { SendView } from "../models/view/sendView";
export class SearchService implements SearchServiceAbstraction {
indexedEntityId?: string = null;
private indexing = false;
private index: lunr.Index = null;
private searchableMinLength = 2;
indexedEntityId?: string = null;
private indexing = false;
private index: lunr.Index = null;
private searchableMinLength = 2;
constructor(private cipherService: CipherService, private logService: LogService,
private i18nService: I18nService) {
if (['zh-CN', 'zh-TW'].indexOf(i18nService.locale) !== -1) {
this.searchableMinLength = 1;
constructor(
private cipherService: CipherService,
private logService: LogService,
private i18nService: I18nService
) {
if (["zh-CN", "zh-TW"].indexOf(i18nService.locale) !== -1) {
this.searchableMinLength = 1;
}
}
clearIndex(): void {
this.indexedEntityId = null;
this.index = null;
}
isSearchable(query: string): boolean {
const notSearchable =
query == null ||
(this.index == null && query.length < this.searchableMinLength) ||
(this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
return !notSearchable;
}
async indexCiphers(indexedEntityId?: string, ciphers?: CipherView[]): Promise<void> {
if (this.indexing) {
return;
}
this.logService.time("search indexing");
this.indexing = true;
this.indexedEntityId = indexedEntityId;
this.index = null;
const builder = new lunr.Builder();
builder.ref("id");
builder.field("shortid", { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) });
builder.field("name", { boost: 10 });
builder.field("subtitle", {
boost: 5,
extractor: (c: CipherView) => {
if (c.subTitle != null && c.type === CipherType.Card) {
return c.subTitle.replace(/\*/g, "");
}
return c.subTitle;
},
});
builder.field("notes");
builder.field("login.username", {
extractor: (c: CipherView) =>
c.type === CipherType.Login && c.login != null ? c.login.username : null,
});
builder.field("login.uris", { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) });
builder.field("fields", { extractor: (c: CipherView) => this.fieldExtractor(c, false) });
builder.field("fields_joined", { extractor: (c: CipherView) => this.fieldExtractor(c, true) });
builder.field("attachments", {
extractor: (c: CipherView) => this.attachmentExtractor(c, false),
});
builder.field("attachments_joined", {
extractor: (c: CipherView) => this.attachmentExtractor(c, true),
});
builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId });
ciphers = ciphers || (await this.cipherService.getAllDecrypted());
ciphers.forEach((c) => builder.add(c));
this.index = builder.build();
this.indexing = false;
this.logService.timeEnd("search indexing");
}
async searchCiphers(
query: string,
filter: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[] = null,
ciphers: CipherView[] = null
): Promise<CipherView[]> {
const results: CipherView[] = [];
if (query != null) {
query = query.trim().toLowerCase();
}
if (query === "") {
query = null;
}
clearIndex(): void {
this.indexedEntityId = null;
this.index = null;
if (ciphers == null) {
ciphers = await this.cipherService.getAllDecrypted();
}
isSearchable(query: string): boolean {
const notSearchable = query == null || (this.index == null && query.length < this.searchableMinLength) ||
(this.index != null && query.length < this.searchableMinLength && query.indexOf('>') !== 0);
return !notSearchable;
if (filter != null && Array.isArray(filter) && filter.length > 0) {
ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c)));
} else if (filter != null) {
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
}
async indexCiphers(indexedEntityId?: string, ciphers?: CipherView[]): Promise<void> {
if (this.indexing) {
return;
}
if (!this.isSearchable(query)) {
return ciphers;
}
this.logService.time('search indexing');
this.indexing = true;
this.indexedEntityId = indexedEntityId;
this.index = null;
const builder = new lunr.Builder();
builder.ref('id');
builder.field('shortid', { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) });
builder.field('name', { boost: 10 });
builder.field('subtitle', {
boost: 5,
extractor: (c: CipherView) => {
if (c.subTitle != null && c.type === CipherType.Card) {
return c.subTitle.replace(/\*/g, '');
}
return c.subTitle;
},
if (this.indexing) {
await new Promise((r) => setTimeout(r, 250));
if (this.indexing) {
await new Promise((r) => setTimeout(r, 500));
}
}
const index = this.getIndexForSearch();
if (index == null) {
// Fall back to basic search if index is not available
return this.searchCiphersBasic(ciphers, query);
}
const ciphersMap = new Map<string, CipherView>();
ciphers.forEach((c) => ciphersMap.set(c.id, c));
let searchResults: lunr.Index.Result[] = null;
const isQueryString = query != null && query.length > 1 && query.indexOf(">") === 0;
if (isQueryString) {
try {
searchResults = index.search(query.substr(1).trim());
} catch (e) {
this.logService.error(e);
}
} else {
// tslint:disable-next-line
const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING;
searchResults = index.query((q) => {
lunr.tokenizer(query).forEach((token) => {
const t = token.toString();
q.term(t, { fields: ["name"], wildcard: soWild });
q.term(t, { fields: ["subtitle"], wildcard: soWild });
q.term(t, { fields: ["login.uris"], wildcard: soWild });
q.term(t, {});
});
builder.field('notes');
builder.field('login.username', {
extractor: (c: CipherView) => c.type === CipherType.Login && c.login != null ? c.login.username : null,
});
builder.field('login.uris', { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) });
builder.field('fields', { extractor: (c: CipherView) => this.fieldExtractor(c, false) });
builder.field('fields_joined', { extractor: (c: CipherView) => this.fieldExtractor(c, true) });
builder.field('attachments', { extractor: (c: CipherView) => this.attachmentExtractor(c, false) });
builder.field('attachments_joined',
{ extractor: (c: CipherView) => this.attachmentExtractor(c, true) });
builder.field('organizationid', { extractor: (c: CipherView) => c.organizationId });
ciphers = ciphers || await this.cipherService.getAllDecrypted();
ciphers.forEach(c => builder.add(c));
this.index = builder.build();
this.indexing = false;
this.logService.timeEnd('search indexing');
});
}
async searchCiphers(query: string,
filter: (((cipher: CipherView) => boolean) | (((cipher: CipherView) => boolean)[])) = null,
ciphers: CipherView[] = null):
Promise<CipherView[]> {
const results: CipherView[] = [];
if (query != null) {
query = query.trim().toLowerCase();
}
if (query === '') {
query = null;
if (searchResults != null) {
searchResults.forEach((r) => {
if (ciphersMap.has(r.ref)) {
results.push(ciphersMap.get(r.ref));
}
});
}
return results;
}
if (ciphers == null) {
ciphers = await this.cipherService.getAllDecrypted();
}
searchCiphersBasic(ciphers: CipherView[], query: string, deleted: boolean = false) {
query = query.trim().toLowerCase();
return ciphers.filter((c) => {
if (deleted !== c.isDeleted) {
return false;
}
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (query.length >= 8 && c.id.startsWith(query)) {
return true;
}
if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) {
return true;
}
if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) {
return true;
}
return false;
});
}
if (filter != null && Array.isArray(filter) && filter.length > 0) {
ciphers = ciphers.filter(c => filter.every(f => f == null || f(c)));
} else if (filter != null) {
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
}
searchSends(sends: SendView[], query: string) {
query = query.trim().toLocaleLowerCase();
if (!this.isSearchable(query)) {
return ciphers;
}
return sends.filter((s) => {
if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (
query.length >= 8 &&
(s.id.startsWith(query) ||
s.accessId.toLocaleLowerCase().startsWith(query) ||
(s.file?.id != null && s.file.id.startsWith(query)))
) {
return true;
}
if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) {
return true;
}
if (s.text?.text != null && s.text.text.toLowerCase().indexOf(query) > -1) {
return true;
}
if (s.file?.fileName != null && s.file.fileName.toLowerCase().indexOf(query) > -1) {
return true;
}
});
}
if (this.indexing) {
await new Promise(r => setTimeout(r, 250));
if (this.indexing) {
await new Promise(r => setTimeout(r, 500));
}
}
getIndexForSearch(): lunr.Index {
return this.index;
}
const index = this.getIndexForSearch();
if (index == null) {
// Fall back to basic search if index is not available
return this.searchCiphersBasic(ciphers, query);
}
private fieldExtractor(c: CipherView, joined: boolean) {
if (!c.hasFields) {
return null;
}
let fields: string[] = [];
c.fields.forEach((f) => {
if (f.name != null) {
fields.push(f.name);
}
if (f.type === FieldType.Text && f.value != null) {
fields.push(f.value);
}
});
fields = fields.filter((f) => f.trim() !== "");
if (fields.length === 0) {
return null;
}
return joined ? fields.join(" ") : fields;
}
const ciphersMap = new Map<string, CipherView>();
ciphers.forEach(c => ciphersMap.set(c.id, c));
let searchResults: lunr.Index.Result[] = null;
const isQueryString = query != null && query.length > 1 && query.indexOf('>') === 0;
if (isQueryString) {
try {
searchResults = index.search(query.substr(1).trim());
} catch (e) {
this.logService.error(e);
}
private attachmentExtractor(c: CipherView, joined: boolean) {
if (!c.hasAttachments) {
return null;
}
let attachments: string[] = [];
c.attachments.forEach((a) => {
if (a != null && a.fileName != null) {
if (joined && a.fileName.indexOf(".") > -1) {
attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf(".")));
} else {
// tslint:disable-next-line
const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING;
searchResults = index.query(q => {
lunr.tokenizer(query).forEach(token => {
const t = token.toString();
q.term(t, { fields: ['name'], wildcard: soWild });
q.term(t, { fields: ['subtitle'], wildcard: soWild });
q.term(t, { fields: ['login.uris'], wildcard: soWild });
q.term(t, {});
});
});
attachments.push(a.fileName);
}
}
});
attachments = attachments.filter((f) => f.trim() !== "");
if (attachments.length === 0) {
return null;
}
return joined ? attachments.join(" ") : attachments;
}
if (searchResults != null) {
searchResults.forEach(r => {
if (ciphersMap.has(r.ref)) {
results.push(ciphersMap.get(r.ref));
}
});
private uriExtractor(c: CipherView) {
if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) {
return null;
}
const uris: string[] = [];
c.login.uris.forEach((u) => {
if (u.uri == null || u.uri === "") {
return;
}
if (u.hostname != null) {
uris.push(u.hostname);
return;
}
let uri = u.uri;
if (u.match !== UriMatchType.RegularExpression) {
const protocolIndex = uri.indexOf("://");
if (protocolIndex > -1) {
uri = uri.substr(protocolIndex + 3);
}
return results;
}
searchCiphersBasic(ciphers: CipherView[], query: string, deleted: boolean = false) {
query = query.trim().toLowerCase();
return ciphers.filter(c => {
if (deleted !== c.isDeleted) {
return false;
}
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (query.length >= 8 && c.id.startsWith(query)) {
return true;
}
if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) {
return true;
}
if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) {
return true;
}
return false;
});
}
searchSends(sends: SendView[], query: string) {
query = query.trim().toLocaleLowerCase();
return sends.filter(s => {
if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (query.length >= 8 && (s.id.startsWith(query) || s.accessId.toLocaleLowerCase().startsWith(query) || (s.file?.id != null && s.file.id.startsWith(query)))) {
return true;
}
if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) {
return true;
}
if (s.text?.text != null && s.text.text.toLowerCase().indexOf(query) > -1) {
return true;
}
if (s.file?.fileName != null && s.file.fileName.toLowerCase().indexOf(query) > -1) {
return true;
}
});
}
getIndexForSearch(): lunr.Index {
return this.index;
}
private fieldExtractor(c: CipherView, joined: boolean) {
if (!c.hasFields) {
return null;
const queryIndex = uri.search(/\?|&|#/);
if (queryIndex > -1) {
uri = uri.substring(0, queryIndex);
}
let fields: string[] = [];
c.fields.forEach(f => {
if (f.name != null) {
fields.push(f.name);
}
if (f.type === FieldType.Text && f.value != null) {
fields.push(f.value);
}
});
fields = fields.filter(f => f.trim() !== '');
if (fields.length === 0) {
return null;
}
return joined ? fields.join(' ') : fields;
}
private attachmentExtractor(c: CipherView, joined: boolean) {
if (!c.hasAttachments) {
return null;
}
let attachments: string[] = [];
c.attachments.forEach(a => {
if (a != null && a.fileName != null) {
if (joined && a.fileName.indexOf('.') > -1) {
attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf('.')));
} else {
attachments.push(a.fileName);
}
}
});
attachments = attachments.filter(f => f.trim() !== '');
if (attachments.length === 0) {
return null;
}
return joined ? attachments.join(' ') : attachments;
}
private uriExtractor(c: CipherView) {
if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) {
return null;
}
const uris: string[] = [];
c.login.uris.forEach(u => {
if (u.uri == null || u.uri === '') {
return;
}
if (u.hostname != null) {
uris.push(u.hostname);
return;
}
let uri = u.uri;
if (u.match !== UriMatchType.RegularExpression) {
const protocolIndex = uri.indexOf('://');
if (protocolIndex > -1) {
uri = uri.substr(protocolIndex + 3);
}
const queryIndex = uri.search(/\?|&|#/);
if (queryIndex > -1) {
uri = uri.substring(0, queryIndex);
}
}
uris.push(uri);
});
return uris.length > 0 ? uris : null;
}
}
uris.push(uri);
});
return uris.length > 0 ? uris : null;
}
}

View File

@@ -1,266 +1,301 @@
import { SendData } from '../models/data/sendData';
import { SendData } from "../models/data/sendData";
import { SendRequest } from '../models/request/sendRequest';
import { SendRequest } from "../models/request/sendRequest";
import { ErrorResponse } from '../models/response/errorResponse';
import { SendResponse } from '../models/response/sendResponse';
import { ErrorResponse } from "../models/response/errorResponse";
import { SendResponse } from "../models/response/sendResponse";
import { EncArrayBuffer } from '../models/domain/encArrayBuffer';
import { EncString } from '../models/domain/encString';
import { Send } from '../models/domain/send';
import { SendFile } from '../models/domain/sendFile';
import { SendText } from '../models/domain/sendText';
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { EncString } from "../models/domain/encString";
import { Send } from "../models/domain/send";
import { SendFile } from "../models/domain/sendFile";
import { SendText } from "../models/domain/sendText";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { SendType } from '../enums/sendType';
import { SendType } from "../enums/sendType";
import { SendView } from '../models/view/sendView';
import { SendView } from "../models/view/sendView";
import { ApiService } from '../abstractions/api.service';
import { CryptoService } from '../abstractions/crypto.service';
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { FileUploadService } from '../abstractions/fileUpload.service';
import { I18nService } from '../abstractions/i18n.service';
import { SendService as SendServiceAbstraction } from '../abstractions/send.service';
import { StateService } from '../abstractions/state.service';
import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { FileUploadService } from "../abstractions/fileUpload.service";
import { I18nService } from "../abstractions/i18n.service";
import { SendService as SendServiceAbstraction } from "../abstractions/send.service";
import { StateService } from "../abstractions/state.service";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
export class SendService implements SendServiceAbstraction {
constructor(private cryptoService: CryptoService, private apiService: ApiService,
private fileUploadService: FileUploadService, private i18nService: I18nService,
private cryptoFunctionService: CryptoFunctionService, private stateService: StateService) { }
constructor(
private cryptoService: CryptoService,
private apiService: ApiService,
private fileUploadService: FileUploadService,
private i18nService: I18nService,
private cryptoFunctionService: CryptoFunctionService,
private stateService: StateService
) {}
async clearCache(): Promise<void> {
await this.stateService.setDecryptedSends(null);
async clearCache(): Promise<void> {
await this.stateService.setDecryptedSends(null);
}
async encrypt(
model: SendView,
file: File | ArrayBuffer,
password: string,
key?: SymmetricCryptoKey
): Promise<[Send, EncArrayBuffer]> {
let fileData: EncArrayBuffer = null;
const send = new Send();
send.id = model.id;
send.type = model.type;
send.disabled = model.disabled;
send.hideEmail = model.hideEmail;
send.maxAccessCount = model.maxAccessCount;
if (model.key == null) {
model.key = await this.cryptoFunctionService.randomBytes(16);
model.cryptoKey = await this.cryptoService.makeSendKey(model.key);
}
async encrypt(model: SendView, file: File | ArrayBuffer, password: string,
key?: SymmetricCryptoKey): Promise<[Send, EncArrayBuffer]> {
let fileData: EncArrayBuffer = null;
const send = new Send();
send.id = model.id;
send.type = model.type;
send.disabled = model.disabled;
send.hideEmail = model.hideEmail;
send.maxAccessCount = model.maxAccessCount;
if (model.key == null) {
model.key = await this.cryptoFunctionService.randomBytes(16);
model.cryptoKey = await this.cryptoService.makeSendKey(model.key);
}
if (password != null) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(password, model.key, 'sha256', 100000);
send.password = Utils.fromBufferToB64(passwordHash);
}
send.key = await this.cryptoService.encrypt(model.key, key);
send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey);
send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey);
if (send.type === SendType.Text) {
send.text = new SendText();
send.text.text = await this.cryptoService.encrypt(model.text.text, model.cryptoKey);
send.text.hidden = model.text.hidden;
} else if (send.type === SendType.File) {
send.file = new SendFile();
if (file != null) {
if (file instanceof ArrayBuffer) {
const [name, data] = await this.encryptFileData(model.file.fileName, file, model.cryptoKey);
send.file.fileName = name;
fileData = data;
} else {
fileData = await this.parseFile(send, file, model.cryptoKey);
}
}
}
return [send, fileData];
if (password != null) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(
password,
model.key,
"sha256",
100000
);
send.password = Utils.fromBufferToB64(passwordHash);
}
async get(id: string): Promise<Send> {
const sends = await this.stateService.getEncryptedSends();
if (sends == null || !sends.hasOwnProperty(id)) {
return null;
}
return new Send(sends[id]);
}
async getAll(): Promise<Send[]> {
const sends = await this.stateService.getEncryptedSends();
const response: Send[] = [];
for (const id in sends) {
if (sends.hasOwnProperty(id)) {
response.push(new Send(sends[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<SendView[]> {
let decSends = await this.stateService.getDecryptedSends();
if (decSends != null) {
return decSends;
}
decSends = [];
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error('No key.');
}
const promises: Promise<any>[] = [];
const sends = await this.getAll();
sends.forEach(send => {
promises.push(send.decrypt().then(f => decSends.push(f)));
});
await Promise.all(promises);
decSends.sort(Utils.getSortFunction(this.i18nService, 'name'));
await this.stateService.setDecryptedSends(decSends);
return decSends;
}
async saveWithServer(sendData: [Send, EncArrayBuffer]): Promise<any> {
const request = new SendRequest(sendData[0], sendData[1]?.buffer.byteLength);
let response: SendResponse;
if (sendData[0].id == null) {
if (sendData[0].type === SendType.Text) {
response = await this.apiService.postSend(request);
} else {
try {
const uploadDataResponse = await this.apiService.postFileTypeSend(request);
response = uploadDataResponse.sendResponse;
await this.fileUploadService.uploadSendFile(uploadDataResponse, sendData[0].file.fileName, sendData[1]);
} catch (e) {
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
response = await this.legacyServerSendFileUpload(sendData, request);
} else if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
throw e;
}
}
}
sendData[0].id = response.id;
sendData[0].accessId = response.accessId;
send.key = await this.cryptoService.encrypt(model.key, key);
send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey);
send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey);
if (send.type === SendType.Text) {
send.text = new SendText();
send.text.text = await this.cryptoService.encrypt(model.text.text, model.cryptoKey);
send.text.hidden = model.text.hidden;
} else if (send.type === SendType.File) {
send.file = new SendFile();
if (file != null) {
if (file instanceof ArrayBuffer) {
const [name, data] = await this.encryptFileData(
model.file.fileName,
file,
model.cryptoKey
);
send.file.fileName = name;
fileData = data;
} else {
response = await this.apiService.putSend(sendData[0].id, request);
fileData = await this.parseFile(send, file, model.cryptoKey);
}
const userId = await this.stateService.getUserId();
const data = new SendData(response, userId);
await this.upsert(data);
}
}
/**
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
* This method still exists for backward compatibility with old server versions.
*/
async legacyServerSendFileUpload(sendData: [Send, EncArrayBuffer], request: SendRequest): Promise<SendResponse>
{
const fd = new FormData();
return [send, fileData];
}
async get(id: string): Promise<Send> {
const sends = await this.stateService.getEncryptedSends();
if (sends == null || !sends.hasOwnProperty(id)) {
return null;
}
return new Send(sends[id]);
}
async getAll(): Promise<Send[]> {
const sends = await this.stateService.getEncryptedSends();
const response: Send[] = [];
for (const id in sends) {
if (sends.hasOwnProperty(id)) {
response.push(new Send(sends[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<SendView[]> {
let decSends = await this.stateService.getDecryptedSends();
if (decSends != null) {
return decSends;
}
decSends = [];
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error("No key.");
}
const promises: Promise<any>[] = [];
const sends = await this.getAll();
sends.forEach((send) => {
promises.push(send.decrypt().then((f) => decSends.push(f)));
});
await Promise.all(promises);
decSends.sort(Utils.getSortFunction(this.i18nService, "name"));
await this.stateService.setDecryptedSends(decSends);
return decSends;
}
async saveWithServer(sendData: [Send, EncArrayBuffer]): Promise<any> {
const request = new SendRequest(sendData[0], sendData[1]?.buffer.byteLength);
let response: SendResponse;
if (sendData[0].id == null) {
if (sendData[0].type === SendType.Text) {
response = await this.apiService.postSend(request);
} else {
try {
const blob = new Blob([sendData[1].buffer], { type: 'application/octet-stream' });
fd.append('model', JSON.stringify(request));
fd.append('data', blob, sendData[0].file.fileName.encryptedString);
const uploadDataResponse = await this.apiService.postFileTypeSend(request);
response = uploadDataResponse.sendResponse;
await this.fileUploadService.uploadSendFile(
uploadDataResponse,
sendData[0].file.fileName,
sendData[1]
);
} catch (e) {
if (Utils.isNode && !Utils.isBrowser) {
fd.append('model', JSON.stringify(request));
fd.append('data', Buffer.from(sendData[1].buffer) as any, {
filepath: sendData[0].file.fileName.encryptedString,
contentType: 'application/octet-stream',
} as any);
} else {
throw e;
}
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
response = await this.legacyServerSendFileUpload(sendData, request);
} else if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
throw e;
}
}
return await this.apiService.postSendFileLegacy(fd);
}
sendData[0].id = response.id;
sendData[0].accessId = response.accessId;
} else {
response = await this.apiService.putSend(sendData[0].id, request);
}
async upsert(send: SendData | SendData[]): Promise<any> {
let sends = await this.stateService.getEncryptedSends();
if (sends == null) {
sends = {};
const userId = await this.stateService.getUserId();
const data = new SendData(response, userId);
await this.upsert(data);
}
/**
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
* This method still exists for backward compatibility with old server versions.
*/
async legacyServerSendFileUpload(
sendData: [Send, EncArrayBuffer],
request: SendRequest
): Promise<SendResponse> {
const fd = new FormData();
try {
const blob = new Blob([sendData[1].buffer], { type: "application/octet-stream" });
fd.append("model", JSON.stringify(request));
fd.append("data", blob, sendData[0].file.fileName.encryptedString);
} catch (e) {
if (Utils.isNode && !Utils.isBrowser) {
fd.append("model", JSON.stringify(request));
fd.append(
"data",
Buffer.from(sendData[1].buffer) as any,
{
filepath: sendData[0].file.fileName.encryptedString,
contentType: "application/octet-stream",
} as any
);
} else {
throw e;
}
}
return await this.apiService.postSendFileLegacy(fd);
}
async upsert(send: SendData | SendData[]): Promise<any> {
let sends = await this.stateService.getEncryptedSends();
if (sends == null) {
sends = {};
}
if (send instanceof SendData) {
const s = send as SendData;
sends[s.id] = s;
} else {
(send as SendData[]).forEach((s) => {
sends[s.id] = s;
});
}
await this.replace(sends);
}
async replace(sends: { [id: string]: SendData }): Promise<any> {
await this.stateService.setDecryptedSends(null);
await this.stateService.setEncryptedSends(sends);
}
async clear(): Promise<any> {
await this.stateService.setDecryptedSends(null);
await this.stateService.setEncryptedSends(null);
}
async delete(id: string | string[]): Promise<any> {
const sends = await this.stateService.getEncryptedSends();
if (sends == null) {
return;
}
if (typeof id === "string") {
if (sends[id] == null) {
return;
}
delete sends[id];
} else {
(id as string[]).forEach((i) => {
delete sends[i];
});
}
await this.replace(sends);
}
async deleteWithServer(id: string): Promise<any> {
await this.apiService.deleteSend(id);
await this.delete(id);
}
async removePasswordWithServer(id: string): Promise<any> {
const response = await this.apiService.putSendRemovePassword(id);
const userId = await this.stateService.getUserId();
const data = new SendData(response, userId);
await this.upsert(data);
}
private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = async (evt) => {
try {
const [name, data] = await this.encryptFileData(
file.name,
evt.target.result as ArrayBuffer,
key
);
send.file.fileName = name;
resolve(data);
} catch (e) {
reject(e);
}
};
reader.onerror = () => {
reject("Error reading file.");
};
});
}
if (send instanceof SendData) {
const s = send as SendData;
sends[s.id] = s;
} else {
(send as SendData[]).forEach(s => {
sends[s.id] = s;
});
}
await this.replace(sends);
}
async replace(sends: { [id: string]: SendData; }): Promise<any> {
await this.stateService.setDecryptedSends(null);
await this.stateService.setEncryptedSends(sends);
}
async clear(): Promise<any> {
await this.stateService.setDecryptedSends(null);
await this.stateService.setEncryptedSends(null);
}
async delete(id: string | string[]): Promise<any> {
const sends = await this.stateService.getEncryptedSends();
if (sends == null) {
return;
}
if (typeof id === 'string') {
if (sends[id] == null) {
return;
}
delete sends[id];
} else {
(id as string[]).forEach(i => {
delete sends[i];
});
}
await this.replace(sends);
}
async deleteWithServer(id: string): Promise<any> {
await this.apiService.deleteSend(id);
await this.delete(id);
}
async removePasswordWithServer(id: string): Promise<any> {
const response = await this.apiService.putSendRemovePassword(id);
const userId = await this.stateService.getUserId();
const data = new SendData(response, userId);
await this.upsert(data);
}
private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = async evt => {
try {
const [name, data] = await this.encryptFileData(file.name, evt.target.result as ArrayBuffer, key);
send.file.fileName = name;
resolve(data);
} catch (e) {
reject(e);
}
};
reader.onerror = () => {
reject('Error reading file.');
};
});
}
private async encryptFileData(fileName: string, data: ArrayBuffer,
key: SymmetricCryptoKey): Promise<[EncString, EncArrayBuffer]> {
const encFileName = await this.cryptoService.encrypt(fileName, key);
const encFileData = await this.cryptoService.encryptToBytes(data, key);
return [encFileName, encFileData];
}
private async encryptFileData(
fileName: string,
data: ArrayBuffer,
key: SymmetricCryptoKey
): Promise<[EncString, EncArrayBuffer]> {
const encFileName = await this.cryptoService.encrypt(fileName, key);
const encFileData = await this.cryptoService.encryptToBytes(data, key);
return [encFileName, encFileData];
}
}

View File

@@ -1,56 +1,55 @@
import { SettingsService as SettingsServiceAbstraction } from '../abstractions/settings.service';
import { StateService } from '../abstractions/state.service';
import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service";
import { StateService } from "../abstractions/state.service";
const Keys = {
settingsPrefix: 'settings_',
equivalentDomains: 'equivalentDomains',
settingsPrefix: "settings_",
equivalentDomains: "equivalentDomains",
};
export class SettingsService implements SettingsServiceAbstraction {
constructor(private stateService: StateService) {
constructor(private stateService: StateService) {}
async clearCache(): Promise<void> {
await this.stateService.setSettings(null);
}
getEquivalentDomains(): Promise<any> {
return this.getSettingsKey(Keys.equivalentDomains);
}
async setEquivalentDomains(equivalentDomains: string[][]): Promise<void> {
await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains);
}
async clear(userId?: string): Promise<void> {
await this.stateService.setSettings(null, { userId: userId });
}
// Helpers
private async getSettings(): Promise<any> {
const settings = await this.stateService.getSettings();
if (settings == null) {
const userId = await this.stateService.getUserId();
}
return settings;
}
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> {
let settings = await this.getSettings();
if (!settings) {
settings = {};
}
async clearCache(): Promise<void> {
await this.stateService.setSettings(null);
}
getEquivalentDomains(): Promise<any> {
return this.getSettingsKey(Keys.equivalentDomains);
}
async setEquivalentDomains(equivalentDomains: string[][]): Promise<void> {
await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains);
}
async clear(userId?: string): Promise<void> {
await this.stateService.setSettings(null, { userId: userId });
}
// Helpers
private async getSettings(): Promise<any> {
const settings = await this.stateService.getSettings();
if (settings == null) {
const userId = await this.stateService.getUserId();
}
return settings;
}
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> {
let settings = await this.getSettings();
if (!settings) {
settings = {};
}
settings[key] = value;
await this.stateService.setSettings(settings);
}
settings[key] = value;
await this.stateService.setSettings(settings);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,335 +1,513 @@
import { StorageService } from '../abstractions/storage.service';
import { StorageService } from "../abstractions/storage.service";
import { Account } from '../models/domain/account';
import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory';
import { State } from '../models/domain/state';
import { StorageOptions } from '../models/domain/storageOptions';
import { Account } from "../models/domain/account";
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
import { State } from "../models/domain/state";
import { StorageOptions } from "../models/domain/storageOptions";
import { CipherData } from '../models/data/cipherData';
import { CollectionData } from '../models/data/collectionData';
import { EventData } from '../models/data/eventData';
import { FolderData } from '../models/data/folderData';
import { OrganizationData } from '../models/data/organizationData';
import { PolicyData } from '../models/data/policyData';
import { ProviderData } from '../models/data/providerData';
import { SendData } from '../models/data/sendData';
import { CipherData } from "../models/data/cipherData";
import { CollectionData } from "../models/data/collectionData";
import { EventData } from "../models/data/eventData";
import { FolderData } from "../models/data/folderData";
import { OrganizationData } from "../models/data/organizationData";
import { PolicyData } from "../models/data/policyData";
import { ProviderData } from "../models/data/providerData";
import { SendData } from "../models/data/sendData";
import { HtmlStorageLocation } from '../enums/htmlStorageLocation';
import { KdfType } from '../enums/kdfType';
import { HtmlStorageLocation } from "../enums/htmlStorageLocation";
import { KdfType } from "../enums/kdfType";
// Originally (before January 2022) storage was handled as a flat key/value pair store.
// With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration.
const v1Keys = {
accessToken: 'accessToken',
alwaysShowDock: 'alwaysShowDock',
autoConfirmFingerprints: 'autoConfirmFingerprints',
autoFillOnPageLoadDefault: 'autoFillOnPageLoadDefault',
biometricAwaitingAcceptance: 'biometricAwaitingAcceptance',
biometricFingerprintValidated: 'biometricFingerprintValidated',
biometricText: 'biometricText',
biometricUnlock: 'biometric',
clearClipboard: 'clearClipboardKey',
clientId: 'clientId',
clientSecret: 'clientSecret',
collapsedGroupings: 'collapsedGroupings',
convertAccountToKeyConnector: 'convertAccountToKeyConnector',
defaultUriMatch: 'defaultUriMatch',
disableAddLoginNotification: 'disableAddLoginNotification',
disableAutoBiometricsPrompt: 'noAutoPromptBiometrics',
disableAutoTotpCopy: 'disableAutoTotpCopy',
disableBadgeCounter: 'disableBadgeCounter',
disableChangedPasswordNotification: 'disableChangedPasswordNotification',
disableContextMenuItem: 'disableContextMenuItem',
disableFavicon: 'disableFavicon',
disableGa: 'disableGa',
dontShowCardsCurrentTab: 'dontShowCardsCurrentTab',
dontShowIdentitiesCurrentTab: 'dontShowIdentitiesCurrentTab',
emailVerified: 'emailVerified',
enableAlwaysOnTop: 'enableAlwaysOnTopKey',
enableAutoFillOnPageLoad: 'enableAutoFillOnPageLoad',
enableBiometric: 'enabledBiometric',
enableBrowserIntegration: 'enableBrowserIntegration',
enableBrowserIntegrationFingerprint: 'enableBrowserIntegrationFingerprint',
enableCloseToTray: 'enableCloseToTray',
enableFullWidth: 'enableFullWidth',
enableGravatars: 'enableGravatars',
enableMinimizeToTray: 'enableMinimizeToTray',
enableStartToTray: 'enableStartToTrayKey',
enableTray: 'enableTray',
encKey: 'encKey', // Generated Symmetric Key
encOrgKeys: 'encOrgKeys',
encPrivate: 'encPrivateKey',
encProviderKeys: 'encProviderKeys',
entityId: 'entityId',
entityType: 'entityType',
environmentUrls: 'environmentUrls',
equivalentDomains: 'equivalentDomains',
eventCollection: 'eventCollection',
forcePasswordReset: 'forcePasswordReset',
history: 'generatedPasswordHistory',
installedVersion: 'installedVersion',
kdf: 'kdf',
kdfIterations: 'kdfIterations',
key: 'key', // Master Key
keyHash: 'keyHash',
lastActive: 'lastActive',
localData: 'sitesLocalData',
locale: 'locale',
mainWindowSize: 'mainWindowSize',
minimizeOnCopyToClipboard: 'minimizeOnCopyToClipboardKey',
neverDomains: 'neverDomains',
noAutoPromptBiometricsText: 'noAutoPromptBiometricsText',
openAtLogin: 'openAtLogin',
passwordGenerationOptions: 'passwordGenerationOptions',
pinProtected: 'pinProtectedKey',
protectedPin: 'protectedPin',
refreshToken: 'refreshToken',
ssoCodeVerifier: 'ssoCodeVerifier',
ssoIdentifier: 'ssoOrgIdentifier',
ssoState: 'ssoState',
stamp: 'securityStamp',
theme: 'theme',
userEmail: 'userEmail',
userId: 'userId',
usesConnector: 'usesKeyConnector',
vaultTimeoutAction: 'vaultTimeoutAction',
vaultTimeout: 'lockOption',
rememberedEmail: 'rememberedEmail',
accessToken: "accessToken",
alwaysShowDock: "alwaysShowDock",
autoConfirmFingerprints: "autoConfirmFingerprints",
autoFillOnPageLoadDefault: "autoFillOnPageLoadDefault",
biometricAwaitingAcceptance: "biometricAwaitingAcceptance",
biometricFingerprintValidated: "biometricFingerprintValidated",
biometricText: "biometricText",
biometricUnlock: "biometric",
clearClipboard: "clearClipboardKey",
clientId: "clientId",
clientSecret: "clientSecret",
collapsedGroupings: "collapsedGroupings",
convertAccountToKeyConnector: "convertAccountToKeyConnector",
defaultUriMatch: "defaultUriMatch",
disableAddLoginNotification: "disableAddLoginNotification",
disableAutoBiometricsPrompt: "noAutoPromptBiometrics",
disableAutoTotpCopy: "disableAutoTotpCopy",
disableBadgeCounter: "disableBadgeCounter",
disableChangedPasswordNotification: "disableChangedPasswordNotification",
disableContextMenuItem: "disableContextMenuItem",
disableFavicon: "disableFavicon",
disableGa: "disableGa",
dontShowCardsCurrentTab: "dontShowCardsCurrentTab",
dontShowIdentitiesCurrentTab: "dontShowIdentitiesCurrentTab",
emailVerified: "emailVerified",
enableAlwaysOnTop: "enableAlwaysOnTopKey",
enableAutoFillOnPageLoad: "enableAutoFillOnPageLoad",
enableBiometric: "enabledBiometric",
enableBrowserIntegration: "enableBrowserIntegration",
enableBrowserIntegrationFingerprint: "enableBrowserIntegrationFingerprint",
enableCloseToTray: "enableCloseToTray",
enableFullWidth: "enableFullWidth",
enableGravatars: "enableGravatars",
enableMinimizeToTray: "enableMinimizeToTray",
enableStartToTray: "enableStartToTrayKey",
enableTray: "enableTray",
encKey: "encKey", // Generated Symmetric Key
encOrgKeys: "encOrgKeys",
encPrivate: "encPrivateKey",
encProviderKeys: "encProviderKeys",
entityId: "entityId",
entityType: "entityType",
environmentUrls: "environmentUrls",
equivalentDomains: "equivalentDomains",
eventCollection: "eventCollection",
forcePasswordReset: "forcePasswordReset",
history: "generatedPasswordHistory",
installedVersion: "installedVersion",
kdf: "kdf",
kdfIterations: "kdfIterations",
key: "key", // Master Key
keyHash: "keyHash",
lastActive: "lastActive",
localData: "sitesLocalData",
locale: "locale",
mainWindowSize: "mainWindowSize",
minimizeOnCopyToClipboard: "minimizeOnCopyToClipboardKey",
neverDomains: "neverDomains",
noAutoPromptBiometricsText: "noAutoPromptBiometricsText",
openAtLogin: "openAtLogin",
passwordGenerationOptions: "passwordGenerationOptions",
pinProtected: "pinProtectedKey",
protectedPin: "protectedPin",
refreshToken: "refreshToken",
ssoCodeVerifier: "ssoCodeVerifier",
ssoIdentifier: "ssoOrgIdentifier",
ssoState: "ssoState",
stamp: "securityStamp",
theme: "theme",
userEmail: "userEmail",
userId: "userId",
usesConnector: "usesKeyConnector",
vaultTimeoutAction: "vaultTimeoutAction",
vaultTimeout: "lockOption",
rememberedEmail: "rememberedEmail",
};
const v1KeyPrefixes = {
ciphers: 'ciphers_',
collections: 'collections_',
folders: 'folders_',
lastSync: 'lastSync_',
policies: 'policies_',
twoFactorToken: 'twoFactorToken_',
organizations: 'organizations_',
providers: 'providers_',
sends: 'sends_',
settings: 'settings_',
ciphers: "ciphers_",
collections: "collections_",
folders: "folders_",
lastSync: "lastSync_",
policies: "policies_",
twoFactorToken: "twoFactorToken_",
organizations: "organizations_",
providers: "providers_",
sends: "sends_",
settings: "settings_",
};
export class StateMigrationService {
readonly latestVersion: number = 2;
readonly latestVersion: number = 2;
constructor(
private storageService: StorageService,
private secureStorageService: StorageService
) {}
constructor(
private storageService: StorageService,
private secureStorageService: StorageService
) {}
async needsMigration(): Promise<boolean> {
const currentStateVersion = (await this.storageService.get<State>('state'))?.globals?.stateVersion;
return currentStateVersion == null || currentStateVersion < this.latestVersion;
async needsMigration(): Promise<boolean> {
const currentStateVersion = (await this.storageService.get<State>("state"))?.globals
?.stateVersion;
return currentStateVersion == null || currentStateVersion < this.latestVersion;
}
async migrate(): Promise<void> {
let currentStateVersion =
(await this.storageService.get<State>("state"))?.globals?.stateVersion ?? 1;
while (currentStateVersion < this.latestVersion) {
switch (currentStateVersion) {
case 1:
await this.migrateStateFrom1To2();
break;
}
currentStateVersion += 1;
}
}
private async migrateStateFrom1To2(): Promise<void> {
const options: StorageOptions = { htmlStorageLocation: HtmlStorageLocation.Local };
const userId = await this.storageService.get<string>("userId");
const initialState: State =
userId == null
? {
globals: {
stateVersion: 2,
},
accounts: {},
activeUserId: null,
}
: {
activeUserId: userId,
globals: {
biometricAwaitingAcceptance: await this.storageService.get<boolean>(
v1Keys.biometricAwaitingAcceptance,
options
),
biometricFingerprintValidated: await this.storageService.get<boolean>(
v1Keys.biometricFingerprintValidated,
options
),
biometricText: await this.storageService.get<string>(v1Keys.biometricText, options),
disableFavicon: await this.storageService.get<boolean>(
v1Keys.disableFavicon,
options
),
enableAlwaysOnTop: await this.storageService.get<boolean>(
v1Keys.enableAlwaysOnTop,
options
),
enableBiometrics: await this.storageService.get<boolean>(
v1Keys.enableBiometric,
options
),
installedVersion: await this.storageService.get<string>(
v1Keys.installedVersion,
options
),
lastActive: await this.storageService.get<number>(v1Keys.lastActive, options),
locale: await this.storageService.get<string>(v1Keys.locale, options),
loginRedirect: null,
mainWindowSize: null,
noAutoPromptBiometrics: await this.storageService.get<boolean>(
v1Keys.disableAutoBiometricsPrompt,
options
),
noAutoPromptBiometricsText: await this.storageService.get<string>(
v1Keys.noAutoPromptBiometricsText,
options
),
openAtLogin: await this.storageService.get<boolean>(v1Keys.openAtLogin, options),
organizationInvitation: await this.storageService.get<string>("", options),
rememberedEmail: await this.storageService.get<string>(
v1Keys.rememberedEmail,
options
),
stateVersion: 2,
theme: await this.storageService.get<string>(v1Keys.theme, options),
twoFactorToken: await this.storageService.get<string>(
v1KeyPrefixes.twoFactorToken + userId,
options
),
vaultTimeout: await this.storageService.get<number>(v1Keys.vaultTimeout, options),
vaultTimeoutAction: await this.storageService.get<string>(
v1Keys.vaultTimeoutAction,
options
),
window: null,
},
accounts: {
[userId]: new Account({
data: {
addEditCipherInfo: null,
ciphers: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: CipherData }>(
v1KeyPrefixes.ciphers + userId,
options
),
},
collapsedGroupings: null,
collections: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: CollectionData }>(
v1KeyPrefixes.collections + userId,
options
),
},
eventCollection: await this.storageService.get<EventData[]>(
v1Keys.eventCollection,
options
),
folders: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: FolderData }>(
v1KeyPrefixes.folders + userId,
options
),
},
localData: null,
organizations: await this.storageService.get<{ [id: string]: OrganizationData }>(
v1KeyPrefixes.organizations + userId
),
passwordGenerationHistory: {
decrypted: null,
encrypted: await this.storageService.get<GeneratedPasswordHistory[]>(
"TODO",
options
), // TODO: Whats up here?
},
policies: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: PolicyData }>(
v1KeyPrefixes.policies + userId,
options
),
},
providers: await this.storageService.get<{ [id: string]: ProviderData }>(
v1KeyPrefixes.providers + userId
),
sends: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: SendData }>(
v1KeyPrefixes.sends,
options
),
},
},
keys: {
apiKeyClientSecret: await this.storageService.get<string>(
v1Keys.clientSecret,
options
),
cryptoMasterKey: null,
cryptoMasterKeyAuto: null,
cryptoMasterKeyB64: null,
cryptoMasterKeyBiometric: null,
cryptoSymmetricKey: {
encrypted: await this.storageService.get<string>(v1Keys.encKey, options),
decrypted: null,
},
legacyEtmKey: null,
organizationKeys: {
decrypted: null,
encrypted: await this.storageService.get<any>(
v1Keys.encOrgKeys + userId,
options
),
},
privateKey: {
decrypted: null,
encrypted: await this.storageService.get<string>(v1Keys.encPrivate, options),
},
providerKeys: {
decrypted: null,
encrypted: await this.storageService.get<any>(
v1Keys.encProviderKeys + userId,
options
),
},
publicKey: null,
},
profile: {
apiKeyClientId: await this.storageService.get<string>(v1Keys.clientId, options),
authenticationStatus: null,
convertAccountToKeyConnector: await this.storageService.get<boolean>(
v1Keys.convertAccountToKeyConnector,
options
),
email: await this.storageService.get<string>(v1Keys.userEmail, options),
emailVerified: await this.storageService.get<boolean>(
v1Keys.emailVerified,
options
),
entityId: null,
entityType: null,
everBeenUnlocked: null,
forcePasswordReset: null,
hasPremiumPersonally: null,
kdfIterations: await this.storageService.get<number>(
v1Keys.kdfIterations,
options
),
kdfType: await this.storageService.get<KdfType>(v1Keys.kdf, options),
keyHash: await this.storageService.get<string>(v1Keys.keyHash, options),
lastActive: await this.storageService.get<number>(v1Keys.lastActive, options),
lastSync: null,
ssoCodeVerifier: await this.storageService.get<string>(
v1Keys.ssoCodeVerifier,
options
),
ssoOrganizationIdentifier: await this.storageService.get<string>(
v1Keys.ssoIdentifier,
options
),
ssoState: null,
userId: userId,
usesKeyConnector: null,
},
settings: {
alwaysShowDock: await this.storageService.get<boolean>(
v1Keys.alwaysShowDock,
options
),
autoConfirmFingerPrints: await this.storageService.get<boolean>(
v1Keys.autoConfirmFingerprints,
options
),
autoFillOnPageLoadDefault: await this.storageService.get<boolean>(
v1Keys.autoFillOnPageLoadDefault,
options
),
biometricLocked: null,
biometricUnlock: await this.storageService.get<boolean>(
v1Keys.biometricUnlock,
options
),
clearClipboard: await this.storageService.get<number>(
v1Keys.clearClipboard,
options
),
defaultUriMatch: await this.storageService.get<any>(
v1Keys.defaultUriMatch,
options
),
disableAddLoginNotification: await this.storageService.get<boolean>(
v1Keys.disableAddLoginNotification,
options
),
disableAutoBiometricsPrompt: await this.storageService.get<boolean>(
v1Keys.disableAutoBiometricsPrompt,
options
),
disableAutoTotpCopy: await this.storageService.get<boolean>(
v1Keys.disableAutoTotpCopy,
options
),
disableBadgeCounter: await this.storageService.get<boolean>(
v1Keys.disableBadgeCounter,
options
),
disableChangedPasswordNotification: await this.storageService.get<boolean>(
v1Keys.disableChangedPasswordNotification,
options
),
disableContextMenuItem: await this.storageService.get<boolean>(
v1Keys.disableContextMenuItem,
options
),
disableGa: await this.storageService.get<boolean>(v1Keys.disableGa, options),
dontShowCardsCurrentTab: await this.storageService.get<boolean>(
v1Keys.dontShowCardsCurrentTab,
options
),
dontShowIdentitiesCurrentTab: await this.storageService.get<boolean>(
v1Keys.dontShowIdentitiesCurrentTab,
options
),
enableAlwaysOnTop: await this.storageService.get<boolean>(
v1Keys.enableAlwaysOnTop,
options
),
enableAutoFillOnPageLoad: await this.storageService.get<boolean>(
v1Keys.enableAutoFillOnPageLoad,
options
),
enableBiometric: await this.storageService.get<boolean>(
v1Keys.enableBiometric,
options
),
enableBrowserIntegration: await this.storageService.get<boolean>(
v1Keys.enableBrowserIntegration,
options
),
enableBrowserIntegrationFingerprint: await this.storageService.get<boolean>(
v1Keys.enableBrowserIntegrationFingerprint,
options
),
enableCloseToTray: await this.storageService.get<boolean>(
v1Keys.enableCloseToTray,
options
),
enableFullWidth: await this.storageService.get<boolean>(
v1Keys.enableFullWidth,
options
),
enableGravitars: await this.storageService.get<boolean>(
v1Keys.enableGravatars,
options
),
enableMinimizeToTray: await this.storageService.get<boolean>(
v1Keys.enableMinimizeToTray,
options
),
enableStartToTray: await this.storageService.get<boolean>(
v1Keys.enableStartToTray,
options
),
enableTray: await this.storageService.get<boolean>(v1Keys.enableTray, options),
environmentUrls: await this.storageService.get<any>(
v1Keys.environmentUrls,
options
),
equivalentDomains: await this.storageService.get<any>(
v1Keys.equivalentDomains,
options
),
minimizeOnCopyToClipboard: await this.storageService.get<boolean>(
v1Keys.minimizeOnCopyToClipboard,
options
),
neverDomains: await this.storageService.get<any>(v1Keys.neverDomains, options),
openAtLogin: await this.storageService.get<boolean>(v1Keys.openAtLogin, options),
passwordGenerationOptions: await this.storageService.get<any>(
v1Keys.passwordGenerationOptions,
options
),
pinProtected: {
decrypted: null,
encrypted: await this.storageService.get<string>(v1Keys.pinProtected, options),
},
protectedPin: await this.storageService.get<string>(v1Keys.protectedPin, options),
settings: await this.storageService.get<any>(
v1KeyPrefixes.settings + userId,
options
),
vaultTimeout: await this.storageService.get<number>(v1Keys.vaultTimeout, options),
vaultTimeoutAction: await this.storageService.get<string>(
v1Keys.vaultTimeoutAction,
options
),
},
tokens: {
accessToken: await this.storageService.get<string>(v1Keys.accessToken, options),
decodedToken: null,
refreshToken: await this.storageService.get<string>(v1Keys.refreshToken, options),
securityStamp: null,
},
}),
},
};
await this.storageService.save("state", initialState, options);
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) {
await this.secureStorageService.save(
`${userId}_masterkey_biometric`,
await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }),
{ keySuffix: "biometric" }
);
await this.secureStorageService.remove(v1Keys.key, { keySuffix: "biometric" });
}
async migrate(): Promise<void> {
let currentStateVersion = (await this.storageService.get<State>('state'))?.globals?.stateVersion ?? 1;
while (currentStateVersion < this.latestVersion) {
switch (currentStateVersion) {
case 1:
await this.migrateStateFrom1To2();
break;
}
currentStateVersion += 1;
}
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) {
await this.secureStorageService.save(
`${userId}_masterkey_auto`,
await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }),
{ keySuffix: "auto" }
);
await this.secureStorageService.remove(v1Keys.key, { keySuffix: "auto" });
}
private async migrateStateFrom1To2(): Promise<void> {
const options: StorageOptions = { htmlStorageLocation: HtmlStorageLocation.Local };
const userId = await this.storageService.get<string>('userId');
const initialState: State = userId == null ?
{
globals: {
stateVersion: 2,
},
accounts: {},
activeUserId: null,
} :
{
activeUserId: userId,
globals: {
biometricAwaitingAcceptance: await this.storageService.get<boolean>(v1Keys.biometricAwaitingAcceptance, options),
biometricFingerprintValidated: await this.storageService.get<boolean>(v1Keys.biometricFingerprintValidated, options),
biometricText: await this.storageService.get<string>(v1Keys.biometricText, options),
disableFavicon: await this.storageService.get<boolean>(v1Keys.disableFavicon, options),
enableAlwaysOnTop: await this.storageService.get<boolean>(v1Keys.enableAlwaysOnTop, options),
enableBiometrics: await this.storageService.get<boolean>(v1Keys.enableBiometric, options),
installedVersion: await this.storageService.get<string>(v1Keys.installedVersion, options),
lastActive: await this.storageService.get<number>(v1Keys.lastActive, options),
locale: await this.storageService.get<string>(v1Keys.locale, options),
loginRedirect: null,
mainWindowSize: null,
noAutoPromptBiometrics: await this.storageService.get<boolean>(v1Keys.disableAutoBiometricsPrompt, options),
noAutoPromptBiometricsText: await this.storageService.get<string>(v1Keys.noAutoPromptBiometricsText, options),
openAtLogin: await this.storageService.get<boolean>(v1Keys.openAtLogin, options),
organizationInvitation: await this.storageService.get<string>('', options),
rememberedEmail: await this.storageService.get<string>(v1Keys.rememberedEmail, options),
stateVersion: 2,
theme: await this.storageService.get<string>(v1Keys.theme, options),
twoFactorToken: await this.storageService.get<string>(v1KeyPrefixes.twoFactorToken + userId, options),
vaultTimeout: await this.storageService.get<number>(v1Keys.vaultTimeout, options),
vaultTimeoutAction: await this.storageService.get<string>(v1Keys.vaultTimeoutAction, options),
window: null,
},
accounts: {
[userId]: new Account({
data: {
addEditCipherInfo: null,
ciphers: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: CipherData }>(v1KeyPrefixes.ciphers + userId, options),
},
collapsedGroupings: null,
collections: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: CollectionData }>(v1KeyPrefixes.collections + userId, options),
},
eventCollection: await this.storageService.get<EventData[]>(v1Keys.eventCollection, options),
folders: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: FolderData }>(v1KeyPrefixes.folders + userId, options),
},
localData: null,
organizations: await this.storageService.get<{ [id: string]: OrganizationData }>(v1KeyPrefixes.organizations + userId),
passwordGenerationHistory: {
decrypted: null,
encrypted: await this.storageService.get<GeneratedPasswordHistory[]>('TODO', options), // TODO: Whats up here?
},
policies: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: PolicyData }>(v1KeyPrefixes.policies + userId, options),
},
providers: await this.storageService.get<{ [id: string]: ProviderData }>(v1KeyPrefixes.providers + userId),
sends: {
decrypted: null,
encrypted: await this.storageService.get<{ [id: string]: SendData }>(v1KeyPrefixes.sends, options),
},
},
keys: {
apiKeyClientSecret: await this.storageService.get<string>(v1Keys.clientSecret, options),
cryptoMasterKey: null,
cryptoMasterKeyAuto: null,
cryptoMasterKeyB64: null,
cryptoMasterKeyBiometric: null,
cryptoSymmetricKey: {
encrypted: await this.storageService.get<string>(v1Keys.encKey, options),
decrypted: null,
},
legacyEtmKey: null,
organizationKeys: {
decrypted: null,
encrypted: await this.storageService.get<any>(v1Keys.encOrgKeys + userId, options),
},
privateKey: {
decrypted: null,
encrypted: await this.storageService.get<string>(v1Keys.encPrivate, options),
},
providerKeys: {
decrypted: null,
encrypted: await this.storageService.get<any>(v1Keys.encProviderKeys + userId, options),
},
publicKey: null,
},
profile: {
apiKeyClientId: await this.storageService.get<string>(v1Keys.clientId, options),
authenticationStatus: null,
convertAccountToKeyConnector: await this.storageService.get<boolean>(v1Keys.convertAccountToKeyConnector, options),
email: await this.storageService.get<string>(v1Keys.userEmail, options),
emailVerified: await this.storageService.get<boolean>(v1Keys.emailVerified, options),
entityId: null,
entityType: null,
everBeenUnlocked: null,
forcePasswordReset: null,
hasPremiumPersonally: null,
kdfIterations: await this.storageService.get<number>(v1Keys.kdfIterations, options),
kdfType: await this.storageService.get<KdfType>(v1Keys.kdf, options),
keyHash: await this.storageService.get<string>(v1Keys.keyHash, options),
lastActive: await this.storageService.get<number>(v1Keys.lastActive, options),
lastSync: null,
ssoCodeVerifier: await this.storageService.get<string>(v1Keys.ssoCodeVerifier, options),
ssoOrganizationIdentifier: await this.storageService.get<string>(v1Keys.ssoIdentifier, options),
ssoState: null,
userId: userId,
usesKeyConnector: null,
},
settings: {
alwaysShowDock: await this.storageService.get<boolean>(v1Keys.alwaysShowDock, options),
autoConfirmFingerPrints: await this.storageService.get<boolean>(v1Keys.autoConfirmFingerprints, options),
autoFillOnPageLoadDefault: await this.storageService.get<boolean>(v1Keys.autoFillOnPageLoadDefault, options),
biometricLocked: null,
biometricUnlock: await this.storageService.get<boolean>(v1Keys.biometricUnlock, options),
clearClipboard: await this.storageService.get<number>(v1Keys.clearClipboard, options),
defaultUriMatch: await this.storageService.get<any>(v1Keys.defaultUriMatch, options),
disableAddLoginNotification: await this.storageService.get<boolean>(v1Keys.disableAddLoginNotification, options),
disableAutoBiometricsPrompt: await this.storageService.get<boolean>(v1Keys.disableAutoBiometricsPrompt, options),
disableAutoTotpCopy: await this.storageService.get<boolean>(v1Keys.disableAutoTotpCopy, options),
disableBadgeCounter: await this.storageService.get<boolean>(v1Keys.disableBadgeCounter, options),
disableChangedPasswordNotification: await this.storageService.get<boolean>(v1Keys.disableChangedPasswordNotification, options),
disableContextMenuItem: await this.storageService.get<boolean>(v1Keys.disableContextMenuItem, options),
disableGa: await this.storageService.get<boolean>(v1Keys.disableGa, options),
dontShowCardsCurrentTab: await this.storageService.get<boolean>(v1Keys.dontShowCardsCurrentTab, options),
dontShowIdentitiesCurrentTab: await this.storageService.get<boolean>(v1Keys.dontShowIdentitiesCurrentTab, options),
enableAlwaysOnTop: await this.storageService.get<boolean>(v1Keys.enableAlwaysOnTop, options),
enableAutoFillOnPageLoad: await this.storageService.get<boolean>(v1Keys.enableAutoFillOnPageLoad, options),
enableBiometric: await this.storageService.get<boolean>(v1Keys.enableBiometric, options),
enableBrowserIntegration: await this.storageService.get<boolean>(v1Keys.enableBrowserIntegration, options),
enableBrowserIntegrationFingerprint: await this.storageService.get<boolean>(v1Keys.enableBrowserIntegrationFingerprint, options),
enableCloseToTray: await this.storageService.get<boolean>(v1Keys.enableCloseToTray, options),
enableFullWidth: await this.storageService.get<boolean>(v1Keys.enableFullWidth, options),
enableGravitars: await this.storageService.get<boolean>(v1Keys.enableGravatars, options),
enableMinimizeToTray: await this.storageService.get<boolean>(v1Keys.enableMinimizeToTray, options),
enableStartToTray: await this.storageService.get<boolean>(v1Keys.enableStartToTray, options),
enableTray: await this.storageService.get<boolean>(v1Keys.enableTray, options),
environmentUrls: await this.storageService.get<any>(v1Keys.environmentUrls, options),
equivalentDomains: await this.storageService.get<any>(v1Keys.equivalentDomains, options),
minimizeOnCopyToClipboard: await this.storageService.get<boolean>(v1Keys.minimizeOnCopyToClipboard, options),
neverDomains: await this.storageService.get<any>(v1Keys.neverDomains, options),
openAtLogin: await this.storageService.get<boolean>(v1Keys.openAtLogin, options),
passwordGenerationOptions: await this.storageService.get<any>(v1Keys.passwordGenerationOptions, options),
pinProtected: {
decrypted: null,
encrypted: await this.storageService.get<string>(v1Keys.pinProtected, options),
},
protectedPin: await this.storageService.get<string>(v1Keys.protectedPin, options),
settings: await this.storageService.get<any>(v1KeyPrefixes.settings + userId, options),
vaultTimeout: await this.storageService.get<number>(v1Keys.vaultTimeout, options),
vaultTimeoutAction: await this.storageService.get<string>(v1Keys.vaultTimeoutAction, options),
},
tokens: {
accessToken: await this.storageService.get<string>(v1Keys.accessToken, options),
decodedToken: null,
refreshToken: await this.storageService.get<string>(v1Keys.refreshToken, options),
securityStamp: null,
},
}),
},
};
await this.storageService.save('state', initialState, options);
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: 'biometric' })) {
await this.secureStorageService.save(
`${userId}_masterkey_biometric`,
await this.secureStorageService.get(v1Keys.key, { keySuffix: 'biometric' }),
{ keySuffix: 'biometric' });
await this.secureStorageService.remove(v1Keys.key, { keySuffix: 'biometric' });
}
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: 'auto' })) {
await this.secureStorageService.save(
`${userId}_masterkey_auto`,
await this.secureStorageService.get(v1Keys.key, { keySuffix: 'auto' }),
{ keySuffix: 'auto' }
);
await this.secureStorageService.remove(v1Keys.key, { keySuffix: 'auto' });
}
if (await this.secureStorageService.has(v1Keys.key)) {
await this.secureStorageService.save(`${userId}_masterkey`, await this.secureStorageService.get(v1Keys.key));
await this.secureStorageService.remove(v1Keys.key);
}
if (await this.secureStorageService.has(v1Keys.key)) {
await this.secureStorageService.save(
`${userId}_masterkey`,
await this.secureStorageService.get(v1Keys.key)
);
await this.secureStorageService.remove(v1Keys.key);
}
}
}

View File

@@ -1,383 +1,401 @@
import { ApiService } from '../abstractions/api.service';
import { CipherService } from '../abstractions/cipher.service';
import { CollectionService } from '../abstractions/collection.service';
import { CryptoService } from '../abstractions/crypto.service';
import { FolderService } from '../abstractions/folder.service';
import { KeyConnectorService } from '../abstractions/keyConnector.service';
import { LogService } from '../abstractions/log.service';
import { MessagingService } from '../abstractions/messaging.service';
import { OrganizationService } from '../abstractions/organization.service';
import { PolicyService } from '../abstractions/policy.service';
import { ProviderService } from '../abstractions/provider.service';
import { SendService } from '../abstractions/send.service';
import { SettingsService } from '../abstractions/settings.service';
import { StateService } from '../abstractions/state.service';
import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service';
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { CollectionService } from "../abstractions/collection.service";
import { CryptoService } from "../abstractions/crypto.service";
import { FolderService } from "../abstractions/folder.service";
import { KeyConnectorService } from "../abstractions/keyConnector.service";
import { LogService } from "../abstractions/log.service";
import { MessagingService } from "../abstractions/messaging.service";
import { OrganizationService } from "../abstractions/organization.service";
import { PolicyService } from "../abstractions/policy.service";
import { ProviderService } from "../abstractions/provider.service";
import { SendService } from "../abstractions/send.service";
import { SettingsService } from "../abstractions/settings.service";
import { StateService } from "../abstractions/state.service";
import { SyncService as SyncServiceAbstraction } from "../abstractions/sync.service";
import { CipherData } from '../models/data/cipherData';
import { CollectionData } from '../models/data/collectionData';
import { FolderData } from '../models/data/folderData';
import { OrganizationData } from '../models/data/organizationData';
import { PolicyData } from '../models/data/policyData';
import { ProviderData } from '../models/data/providerData';
import { SendData } from '../models/data/sendData';
import { CipherData } from "../models/data/cipherData";
import { CollectionData } from "../models/data/collectionData";
import { FolderData } from "../models/data/folderData";
import { OrganizationData } from "../models/data/organizationData";
import { PolicyData } from "../models/data/policyData";
import { ProviderData } from "../models/data/providerData";
import { SendData } from "../models/data/sendData";
import { CipherResponse } from '../models/response/cipherResponse';
import { CollectionDetailsResponse } from '../models/response/collectionResponse';
import { DomainsResponse } from '../models/response/domainsResponse';
import { FolderResponse } from '../models/response/folderResponse';
import { CipherResponse } from "../models/response/cipherResponse";
import { CollectionDetailsResponse } from "../models/response/collectionResponse";
import { DomainsResponse } from "../models/response/domainsResponse";
import { FolderResponse } from "../models/response/folderResponse";
import {
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
} from '../models/response/notificationResponse';
import { PolicyResponse } from '../models/response/policyResponse';
import { ProfileResponse } from '../models/response/profileResponse';
import { SendResponse } from '../models/response/sendResponse';
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
} from "../models/response/notificationResponse";
import { PolicyResponse } from "../models/response/policyResponse";
import { ProfileResponse } from "../models/response/profileResponse";
import { SendResponse } from "../models/response/sendResponse";
export class SyncService implements SyncServiceAbstraction {
syncInProgress: boolean = false;
syncInProgress: boolean = false;
constructor(private apiService: ApiService, private settingsService: SettingsService,
private folderService: FolderService, private cipherService: CipherService,
private cryptoService: CryptoService, private collectionService: CollectionService,
private messagingService: MessagingService, private policyService: PolicyService,
private sendService: SendService, private logService: LogService,
private keyConnectorService: KeyConnectorService, private stateService: StateService,
private organizationService: OrganizationService, private providerService: ProviderService,
private logoutCallback: (expired: boolean) => Promise<void>) { }
constructor(
private apiService: ApiService,
private settingsService: SettingsService,
private folderService: FolderService,
private cipherService: CipherService,
private cryptoService: CryptoService,
private collectionService: CollectionService,
private messagingService: MessagingService,
private policyService: PolicyService,
private sendService: SendService,
private logService: LogService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
private organizationService: OrganizationService,
private providerService: ProviderService,
private logoutCallback: (expired: boolean) => Promise<void>
) {}
async getLastSync(): Promise<Date> {
if (await this.stateService.getUserId() == null) {
return null;
}
const lastSync = await this.stateService.getLastSync();
if (lastSync) {
return new Date(lastSync);
}
return null;
async getLastSync(): Promise<Date> {
if ((await this.stateService.getUserId()) == null) {
return null;
}
async setLastSync(date: Date, userId?: string): Promise<any> {
await this.stateService.setLastSync(date.toJSON(), { userId: userId });
const lastSync = await this.stateService.getLastSync();
if (lastSync) {
return new Date(lastSync);
}
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
this.syncStarted();
const isAuthenticated = await this.stateService.getIsAuthenticated();
if (!isAuthenticated) {
return this.syncCompleted(false);
}
return null;
}
const now = new Date();
let needsSync = false;
try {
needsSync = await this.needsSyncing(forceSync);
} catch (e) {
if (allowThrowOnError) {
throw e;
}
}
async setLastSync(date: Date, userId?: string): Promise<any> {
await this.stateService.setLastSync(date.toJSON(), { userId: userId });
}
if (!needsSync) {
await this.setLastSync(now);
return this.syncCompleted(false);
}
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
this.syncStarted();
const isAuthenticated = await this.stateService.getIsAuthenticated();
if (!isAuthenticated) {
return this.syncCompleted(false);
}
const userId = await this.stateService.getUserId();
try {
await this.apiService.refreshIdentityToken();
const response = await this.apiService.getSync();
const now = new Date();
let needsSync = false;
try {
needsSync = await this.needsSyncing(forceSync);
} catch (e) {
if (allowThrowOnError) {
throw e;
}
}
await this.syncProfile(response.profile);
await this.syncFolders(userId, response.folders);
await this.syncCollections(response.collections);
await this.syncCiphers(userId, response.ciphers);
await this.syncSends(userId, response.sends);
await this.syncSettings(response.domains);
await this.syncPolicies(response.policies);
if (!needsSync) {
await this.setLastSync(now);
return this.syncCompleted(false);
}
await this.setLastSync(now);
const userId = await this.stateService.getUserId();
try {
await this.apiService.refreshIdentityToken();
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.syncSends(userId, response.sends);
await this.syncSettings(response.domains);
await this.syncPolicies(response.policies);
await this.setLastSync(now);
return this.syncCompleted(true);
} catch (e) {
if (allowThrowOnError) {
throw e;
} else {
return this.syncCompleted(false);
}
}
}
async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
try {
const localFolder = await this.folderService.get(notification.id);
if (
(!isEdit && localFolder == null) ||
(isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)
) {
const remoteFolder = await this.apiService.getFolder(notification.id);
if (remoteFolder != null) {
const userId = await this.stateService.getUserId();
await this.folderService.upsert(new FolderData(remoteFolder, userId));
this.messagingService.send("syncedUpsertedFolder", { folderId: notification.id });
return this.syncCompleted(true);
} catch (e) {
if (allowThrowOnError) {
throw e;
}
}
} catch (e) {
this.logService.error(e);
}
}
return this.syncCompleted(false);
}
async syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
await this.folderService.delete(notification.id);
this.messagingService.send("syncedDeletedFolder", { folderId: notification.id });
this.syncCompleted(true);
return true;
}
return this.syncCompleted(false);
}
async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
try {
let shouldUpdate = true;
const localCipher = await this.cipherService.get(notification.id);
if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) {
shouldUpdate = false;
}
let checkCollections = false;
if (shouldUpdate) {
if (isEdit) {
shouldUpdate = localCipher != null;
checkCollections = true;
} else {
if (notification.collectionIds == null || notification.organizationId == null) {
shouldUpdate = localCipher == null;
} else {
return this.syncCompleted(false);
shouldUpdate = false;
checkCollections = true;
}
}
}
}
async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
try {
const localFolder = await this.folderService.get(notification.id);
if ((!isEdit && localFolder == null) ||
(isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)) {
const remoteFolder = await this.apiService.getFolder(notification.id);
if (remoteFolder != null) {
const userId = await this.stateService.getUserId();
await this.folderService.upsert(new FolderData(remoteFolder, userId));
this.messagingService.send('syncedUpsertedFolder', { folderId: notification.id });
return this.syncCompleted(true);
}
}
} catch (e) {
this.logService.error(e);
if (
!shouldUpdate &&
checkCollections &&
notification.organizationId != null &&
notification.collectionIds != null &&
notification.collectionIds.length > 0
) {
const collections = await this.collectionService.getAll();
if (collections != null) {
for (let i = 0; i < collections.length; i++) {
if (notification.collectionIds.indexOf(collections[i].id) > -1) {
shouldUpdate = true;
break;
}
}
}
}
return this.syncCompleted(false);
}
async syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
await this.folderService.delete(notification.id);
this.messagingService.send('syncedDeletedFolder', { folderId: notification.id });
this.syncCompleted(true);
return true;
}
return this.syncCompleted(false);
}
async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
try {
let shouldUpdate = true;
const localCipher = await this.cipherService.get(notification.id);
if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) {
shouldUpdate = false;
}
let checkCollections = false;
if (shouldUpdate) {
if (isEdit) {
shouldUpdate = localCipher != null;
checkCollections = true;
} else {
if (notification.collectionIds == null || notification.organizationId == null) {
shouldUpdate = localCipher == null;
} else {
shouldUpdate = false;
checkCollections = true;
}
}
}
if (!shouldUpdate && checkCollections && notification.organizationId != null &&
notification.collectionIds != null && notification.collectionIds.length > 0) {
const collections = await this.collectionService.getAll();
if (collections != null) {
for (let i = 0; i < collections.length; i++) {
if (notification.collectionIds.indexOf(collections[i].id) > -1) {
shouldUpdate = true;
break;
}
}
}
}
if (shouldUpdate) {
const remoteCipher = await this.apiService.getCipher(notification.id);
if (remoteCipher != null) {
const userId = await this.stateService.getUserId();
await this.cipherService.upsert(new CipherData(remoteCipher, userId));
this.messagingService.send('syncedUpsertedCipher', { cipherId: notification.id });
return this.syncCompleted(true);
}
}
} catch (e) {
if (e != null && e.statusCode === 404 && isEdit) {
await this.cipherService.delete(notification.id);
this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id });
return this.syncCompleted(true);
}
}
}
return this.syncCompleted(false);
}
async syncDeleteCipher(notification: SyncCipherNotification): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
await this.cipherService.delete(notification.id);
this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id });
if (shouldUpdate) {
const remoteCipher = await this.apiService.getCipher(notification.id);
if (remoteCipher != null) {
const userId = await this.stateService.getUserId();
await this.cipherService.upsert(new CipherData(remoteCipher, userId));
this.messagingService.send("syncedUpsertedCipher", { cipherId: notification.id });
return this.syncCompleted(true);
}
}
return this.syncCompleted(false);
}
async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
try {
const localSend = await this.sendService.get(notification.id);
if ((!isEdit && localSend == null) ||
(isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)) {
const remoteSend = await this.apiService.getSend(notification.id);
if (remoteSend != null) {
const userId = await this.stateService.getUserId();
await this.sendService.upsert(new SendData(remoteSend, userId));
this.messagingService.send('syncedUpsertedSend', { sendId: notification.id });
return this.syncCompleted(true);
}
}
} catch (e) {
this.logService.error(e);
}
} catch (e) {
if (e != null && e.statusCode === 404 && isEdit) {
await this.cipherService.delete(notification.id);
this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id });
return this.syncCompleted(true);
}
return this.syncCompleted(false);
}
}
return this.syncCompleted(false);
}
async syncDeleteSend(notification: SyncSendNotification): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
await this.sendService.delete(notification.id);
this.messagingService.send('syncedDeletedSend', { sendId: notification.id });
this.syncCompleted(true);
return true;
async syncDeleteCipher(notification: SyncCipherNotification): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
await this.cipherService.delete(notification.id);
this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id });
return this.syncCompleted(true);
}
return this.syncCompleted(false);
}
async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
try {
const localSend = await this.sendService.get(notification.id);
if (
(!isEdit && localSend == null) ||
(isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)
) {
const remoteSend = await this.apiService.getSend(notification.id);
if (remoteSend != null) {
const userId = await this.stateService.getUserId();
await this.sendService.upsert(new SendData(remoteSend, userId));
this.messagingService.send("syncedUpsertedSend", { sendId: notification.id });
return this.syncCompleted(true);
}
}
return this.syncCompleted(false);
} catch (e) {
this.logService.error(e);
}
}
return this.syncCompleted(false);
}
async syncDeleteSend(notification: SyncSendNotification): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
await this.sendService.delete(notification.id);
this.messagingService.send("syncedDeletedSend", { sendId: notification.id });
this.syncCompleted(true);
return true;
}
return this.syncCompleted(false);
}
// Helpers
private syncStarted() {
this.syncInProgress = true;
this.messagingService.send("syncStarted");
}
private syncCompleted(successfully: boolean): boolean {
this.syncInProgress = false;
this.messagingService.send("syncCompleted", { successfully: successfully });
return successfully;
}
private async needsSyncing(forceSync: boolean) {
if (forceSync) {
return true;
}
// Helpers
private syncStarted() {
this.syncInProgress = true;
this.messagingService.send('syncStarted');
const lastSync = await this.getLastSync();
if (lastSync == null || lastSync.getTime() === 0) {
return true;
}
private syncCompleted(successfully: boolean): boolean {
this.syncInProgress = false;
this.messagingService.send('syncCompleted', { successfully: successfully });
return successfully;
const response = await this.apiService.getAccountRevisionDate();
if (new Date(response) <= lastSync) {
return false;
}
return true;
}
private async syncProfile(response: ProfileResponse) {
const stamp = await this.stateService.getSecurityStamp();
if (stamp != null && stamp !== response.securityStamp) {
if (this.logoutCallback != null) {
await this.logoutCallback(true);
}
throw new Error("Stamp has changed");
}
private async needsSyncing(forceSync: boolean) {
if (forceSync) {
return true;
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(response.privateKey);
await this.cryptoService.setProviderKeys(response.providers);
await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations);
await this.stateService.setSecurityStamp(response.securityStamp);
await this.stateService.setEmailVerified(response.emailVerified);
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
const organizations: { [id: string]: OrganizationData } = {};
response.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o);
});
const providers: { [id: string]: ProviderData } = {};
response.providers.forEach((p) => {
providers[p.id] = new ProviderData(p);
});
response.providerOrganizations.forEach((o) => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await Promise.all([
this.organizationService.save(organizations),
this.providerService.save(providers),
]);
if (await this.keyConnectorService.userNeedsMigration()) {
this.messagingService.send("convertAccountToKeyConnector");
} else {
this.keyConnectorService.removeConvertAccountRequired();
}
}
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: CollectionDetailsResponse[]) {
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 syncSends(userId: string, response: SendResponse[]) {
const sends: { [id: string]: SendData } = {};
response.forEach((s) => {
sends[s.id] = new SendData(s, userId);
});
return await this.sendService.replace(sends);
}
private async syncSettings(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);
}
const lastSync = await this.getLastSync();
if (lastSync == null || lastSync.getTime() === 0) {
return true;
}
const response = await this.apiService.getAccountRevisionDate();
if (new Date(response) <= lastSync) {
return false;
}
return true;
});
}
private async syncProfile(response: ProfileResponse) {
const stamp = await this.stateService.getSecurityStamp();
if (stamp != null && stamp !== response.securityStamp) {
if (this.logoutCallback != null) {
await this.logoutCallback(true);
}
return this.settingsService.setEquivalentDomains(eqDomains);
}
throw new Error('Stamp has changed');
}
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(response.privateKey);
await this.cryptoService.setProviderKeys(response.providers);
await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations);
await this.stateService.setSecurityStamp(response.securityStamp);
await this.stateService.setEmailVerified(response.emailVerified);
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
const organizations: { [id: string]: OrganizationData; } = {};
response.organizations.forEach(o => {
organizations[o.id] = new OrganizationData(o);
});
const providers: { [id: string]: ProviderData; } = {};
response.providers.forEach(p => {
providers[p.id] = new ProviderData(p);
});
response.providerOrganizations.forEach(o => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await Promise.all([
this.organizationService.save(organizations),
this.providerService.save(providers),
]);
if (await this.keyConnectorService.userNeedsMigration()) {
this.messagingService.send('convertAccountToKeyConnector');
} else {
this.keyConnectorService.removeConvertAccountRequired();
}
}
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: CollectionDetailsResponse[]) {
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 syncSends(userId: string, response: SendResponse[]) {
const sends: { [id: string]: SendData; } = {};
response.forEach(s => {
sends[s.id] = new SendData(s, userId);
});
return await this.sendService.replace(sends);
}
private async syncSettings(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);
}
private async syncPolicies(response: PolicyResponse[]) {
const policies: { [id: string]: PolicyData; } = {};
if (response != null) {
response.forEach(p => {
policies[p.id] = new PolicyData(p);
});
}
return await this.policyService.replace(policies);
private async syncPolicies(response: PolicyResponse[]) {
const policies: { [id: string]: PolicyData } = {};
if (response != null) {
response.forEach((p) => {
policies[p.id] = new PolicyData(p);
});
}
return await this.policyService.replace(policies);
}
}

View File

@@ -1,85 +1,91 @@
import { MessagingService } from '../abstractions/messaging.service';
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
import { StateService } from '../abstractions/state.service';
import { SystemService as SystemServiceAbstraction } from '../abstractions/system.service';
import { MessagingService } from "../abstractions/messaging.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import { StateService } from "../abstractions/state.service";
import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
export class SystemService implements SystemServiceAbstraction {
private reloadInterval: any = null;
private clearClipboardTimeout: any = null;
private clearClipboardTimeoutFunction: () => Promise<any> = null;
private reloadInterval: any = null;
private clearClipboardTimeout: any = null;
private clearClipboardTimeoutFunction: () => Promise<any> = null;
constructor(private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService,
private reloadCallback: () => Promise<void> = null, private stateService: StateService) {
}
constructor(
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private reloadCallback: () => Promise<void> = null,
private stateService: StateService
) {}
async startProcessReload(): Promise<void> {
if (await this.stateService.getDecryptedPinProtected() != null ||
await this.stateService.getBiometricLocked() ||
this.reloadInterval != null) {
return;
}
this.cancelProcessReload();
this.reloadInterval = setInterval(async () => {
let doRefresh = false;
const lastActive = await this.stateService.getLastActive();
if (lastActive != null) {
const diffSeconds = (new Date()).getTime() - lastActive;
// Don't refresh if they are still active in the window
doRefresh = diffSeconds >= 5000;
}
const biometricLockedFingerprintValidated =
await this.stateService.getBiometricFingerprintValidated() && await this.stateService.getBiometricLocked();
if (doRefresh && !biometricLockedFingerprintValidated) {
clearInterval(this.reloadInterval);
this.reloadInterval = null;
this.messagingService.send('reloadProcess');
if (this.reloadCallback != null) {
await this.reloadCallback();
}
}
}, 10000);
async startProcessReload(): Promise<void> {
if (
(await this.stateService.getDecryptedPinProtected()) != null ||
(await this.stateService.getBiometricLocked()) ||
this.reloadInterval != null
) {
return;
}
this.cancelProcessReload();
this.reloadInterval = setInterval(async () => {
let doRefresh = false;
const lastActive = await this.stateService.getLastActive();
if (lastActive != null) {
const diffSeconds = new Date().getTime() - lastActive;
// Don't refresh if they are still active in the window
doRefresh = diffSeconds >= 5000;
}
const biometricLockedFingerprintValidated =
(await this.stateService.getBiometricFingerprintValidated()) &&
(await this.stateService.getBiometricLocked());
if (doRefresh && !biometricLockedFingerprintValidated) {
clearInterval(this.reloadInterval);
this.reloadInterval = null;
this.messagingService.send("reloadProcess");
if (this.reloadCallback != null) {
await this.reloadCallback();
}
}
}, 10000);
}
cancelProcessReload(): void {
if (this.reloadInterval != null) {
clearInterval(this.reloadInterval);
this.reloadInterval = null;
}
cancelProcessReload(): void {
if (this.reloadInterval != null) {
clearInterval(this.reloadInterval);
this.reloadInterval = null;
}
}
async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise<void> {
if (this.clearClipboardTimeout != null) {
clearTimeout(this.clearClipboardTimeout);
this.clearClipboardTimeout = null;
}
if (Utils.isNullOrWhitespace(clipboardValue)) {
return;
}
await this.stateService.getClearClipboard().then(clearSeconds => {
if (clearSeconds == null) {
return;
}
if (timeoutMs == null) {
timeoutMs = clearSeconds * 1000;
}
this.clearClipboardTimeoutFunction = async () => {
const clipboardValueNow = await this.platformUtilsService.readFromClipboard();
if (clipboardValue === clipboardValueNow) {
this.platformUtilsService.copyToClipboard('', { clearing: true });
}
};
this.clearClipboardTimeout = setTimeout(async () => {
await this.clearPendingClipboard();
}, timeoutMs);
});
async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise<void> {
if (this.clearClipboardTimeout != null) {
clearTimeout(this.clearClipboardTimeout);
this.clearClipboardTimeout = null;
}
if (Utils.isNullOrWhitespace(clipboardValue)) {
return;
}
await this.stateService.getClearClipboard().then((clearSeconds) => {
if (clearSeconds == null) {
return;
}
if (timeoutMs == null) {
timeoutMs = clearSeconds * 1000;
}
this.clearClipboardTimeoutFunction = async () => {
const clipboardValueNow = await this.platformUtilsService.readFromClipboard();
if (clipboardValue === clipboardValueNow) {
this.platformUtilsService.copyToClipboard("", { clearing: true });
}
};
this.clearClipboardTimeout = setTimeout(async () => {
await this.clearPendingClipboard();
}, timeoutMs);
});
}
async clearPendingClipboard() {
if (this.clearClipboardTimeoutFunction != null) {
await this.clearClipboardTimeoutFunction();
this.clearClipboardTimeoutFunction = null;
}
async clearPendingClipboard() {
if (this.clearClipboardTimeoutFunction != null) {
await this.clearClipboardTimeoutFunction();
this.clearClipboardTimeoutFunction = null;
}
}
}

View File

@@ -1,221 +1,224 @@
import { StateService } from '../abstractions/state.service';
import { TokenService as TokenServiceAbstraction } from '../abstractions/token.service';
import { StateService } from "../abstractions/state.service";
import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
export class TokenService implements TokenServiceAbstraction {
constructor(private stateService: StateService) {
constructor(private stateService: StateService) {}
async setTokens(
accessToken: string,
refreshToken: string,
clientIdClientSecret: [string, string]
): Promise<any> {
await this.setToken(accessToken);
await this.setRefreshToken(refreshToken);
if (clientIdClientSecret != null) {
await this.setClientId(clientIdClientSecret[0]);
await this.setClientSecret(clientIdClientSecret[1]);
}
}
async setClientId(clientId: string): Promise<any> {
if ((await this.skipTokenStorage()) || clientId == null) {
return;
}
return await this.stateService.setApiKeyClientId(clientId);
}
async getClientId(): Promise<string> {
return await this.stateService.getApiKeyClientId();
}
async setClientSecret(clientSecret: string): Promise<any> {
if ((await this.skipTokenStorage()) || clientSecret == null) {
return;
}
return await this.stateService.setApiKeyClientSecret(clientSecret);
}
async getClientSecret(): Promise<string> {
return await this.stateService.getApiKeyClientSecret();
}
async setToken(token: string): Promise<void> {
await this.stateService.setAccessToken(token);
}
async getToken(): Promise<string> {
return await this.stateService.getAccessToken();
}
async setRefreshToken(refreshToken: string): Promise<any> {
if (await this.skipTokenStorage()) {
return;
}
return await this.stateService.setRefreshToken(refreshToken);
}
async getRefreshToken(): Promise<string> {
return await this.stateService.getRefreshToken();
}
async toggleTokens(): Promise<any> {
const token = await this.getToken();
const refreshToken = await this.getRefreshToken();
const clientId = await this.getClientId();
const clientSecret = await this.getClientSecret();
const timeout = await this.stateService.getVaultTimeout();
const action = await this.stateService.getVaultTimeoutAction();
if ((timeout != null || timeout === 0) && action === "logOut") {
// if we have a vault timeout and the action is log out, reset tokens
await this.clearToken();
}
async setTokens(accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]): Promise<any> {
await this.setToken(accessToken);
await this.setRefreshToken(refreshToken);
if (clientIdClientSecret != null) {
await this.setClientId(clientIdClientSecret[0]);
await this.setClientSecret(clientIdClientSecret[1]);
}
await this.setToken(token);
await this.setRefreshToken(refreshToken);
await this.setClientId(clientId);
await this.setClientSecret(clientSecret);
}
async setTwoFactorToken(token: string): Promise<any> {
return await this.stateService.setTwoFactorToken(token);
}
async getTwoFactorToken(): Promise<string> {
return await this.stateService.getTwoFactorToken();
}
async clearTwoFactorToken(): Promise<any> {
return await this.stateService.setTwoFactorToken(null);
}
async clearToken(userId?: string): Promise<any> {
await this.stateService.setAccessToken(null, { userId: userId });
await this.stateService.setRefreshToken(null, { userId: userId });
await this.stateService.setApiKeyClientId(null, { userId: userId });
await this.stateService.setApiKeyClientSecret(null, { userId: userId });
}
// jwthelper methods
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
async decodeToken(token?: string): Promise<any> {
const storedToken = await this.stateService.getDecodedToken();
if (token === null && storedToken != null) {
return storedToken;
}
async setClientId(clientId: string): Promise<any> {
if (await this.skipTokenStorage() || clientId == null) {
return;
}
return await this.stateService.setApiKeyClientId(clientId);
token = token ?? (await this.stateService.getAccessToken());
if (token == null) {
throw new Error("Token not found.");
}
async getClientId(): Promise<string> {
return await this.stateService.getApiKeyClientId();
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("JWT must have 3 parts");
}
async setClientSecret(clientSecret: string): Promise<any> {
if (await this.skipTokenStorage() || clientSecret == null) {
return;
}
return await this.stateService.setApiKeyClientSecret(clientSecret);
const decoded = Utils.fromUrlB64ToUtf8(parts[1]);
if (decoded == null) {
throw new Error("Cannot decode the token");
}
async getClientSecret(): Promise<string> {
return await this.stateService.getApiKeyClientSecret();
const decodedToken = JSON.parse(decoded);
return decodedToken;
}
async getTokenExpirationDate(): Promise<Date> {
const decoded = await this.decodeToken();
if (typeof decoded.exp === "undefined") {
return null;
}
async setToken(token: string): Promise<void> {
await this.stateService.setAccessToken(token);
const d = new Date(0); // The 0 here is the key, which sets the date to the epoch
d.setUTCSeconds(decoded.exp);
return d;
}
async tokenSecondsRemaining(offsetSeconds: number = 0): Promise<number> {
const d = await this.getTokenExpirationDate();
if (d == null) {
return 0;
}
async getToken(): Promise<string> {
return await this.stateService.getAccessToken();
const msRemaining = d.valueOf() - (new Date().valueOf() + offsetSeconds * 1000);
return Math.round(msRemaining / 1000);
}
async tokenNeedsRefresh(minutes: number = 5): Promise<boolean> {
const sRemaining = await this.tokenSecondsRemaining();
return sRemaining < 60 * minutes;
}
async getUserId(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.sub === "undefined") {
throw new Error("No user id found");
}
async setRefreshToken(refreshToken: string): Promise<any> {
if (await this.skipTokenStorage()) {
return;
}
return await this.stateService.setRefreshToken(refreshToken);
return decoded.sub as string;
}
async getEmail(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.email === "undefined") {
throw new Error("No email found");
}
async getRefreshToken(): Promise<string> {
return await this.stateService.getRefreshToken();
return decoded.email as string;
}
async getEmailVerified(): Promise<boolean> {
const decoded = await this.decodeToken();
if (typeof decoded.email_verified === "undefined") {
throw new Error("No email verification found");
}
async toggleTokens(): Promise<any> {
const token = await this.getToken();
const refreshToken = await this.getRefreshToken();
const clientId = await this.getClientId();
const clientSecret = await this.getClientSecret();
const timeout = await this.stateService.getVaultTimeout();
const action = await this.stateService.getVaultTimeoutAction();
return decoded.email_verified as boolean;
}
if ((timeout != null || timeout === 0) && action === 'logOut') {
// if we have a vault timeout and the action is log out, reset tokens
await this.clearToken();
}
await this.setToken(token);
await this.setRefreshToken(refreshToken);
await this.setClientId(clientId);
await this.setClientSecret(clientSecret);
async getName(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.name === "undefined") {
return null;
}
async setTwoFactorToken(token: string): Promise<any> {
return await this.stateService.setTwoFactorToken(token);
return decoded.name as string;
}
async getPremium(): Promise<boolean> {
const decoded = await this.decodeToken();
if (typeof decoded.premium === "undefined") {
return false;
}
async getTwoFactorToken(): Promise<string> {
return await this.stateService.getTwoFactorToken();
return decoded.premium as boolean;
}
async getIssuer(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.iss === "undefined") {
throw new Error("No issuer found");
}
async clearTwoFactorToken(): Promise<any> {
return await this.stateService.setTwoFactorToken(null);
return decoded.iss as string;
}
async getIsExternal(): Promise<boolean> {
const decoded = await this.decodeToken();
if (!Array.isArray(decoded.amr)) {
throw new Error("No amr found");
}
async clearToken(userId?: string): Promise<any> {
await this.stateService.setAccessToken(null, { userId: userId });
await this.stateService.setRefreshToken(null, { userId: userId });
await this.stateService.setApiKeyClientId(null, { userId: userId });
await this.stateService.setApiKeyClientSecret(null, { userId: userId });
}
return decoded.amr.includes("external");
}
// jwthelper methods
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
async decodeToken(token?: string): Promise<any> {
const storedToken = await this.stateService.getDecodedToken();
if (token === null && storedToken != null) {
return storedToken;
}
token = token ?? await this.stateService.getAccessToken();
if (token == null) {
throw new Error('Token not found.');
}
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('JWT must have 3 parts');
}
const decoded = Utils.fromUrlB64ToUtf8(parts[1]);
if (decoded == null) {
throw new Error('Cannot decode the token');
}
const decodedToken = JSON.parse(decoded);
return decodedToken;
}
async getTokenExpirationDate(): Promise<Date> {
const decoded = await 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;
}
async tokenSecondsRemaining(offsetSeconds: number = 0): Promise<number> {
const d = await this.getTokenExpirationDate();
if (d == null) {
return 0;
}
const msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000));
return Math.round(msRemaining / 1000);
}
async tokenNeedsRefresh(minutes: number = 5): Promise<boolean> {
const sRemaining = await this.tokenSecondsRemaining();
return sRemaining < (60 * minutes);
}
async getUserId(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.sub === 'undefined') {
throw new Error('No user id found');
}
return decoded.sub as string;
}
async getEmail(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.email === 'undefined') {
throw new Error('No email found');
}
return decoded.email as string;
}
async getEmailVerified(): Promise<boolean> {
const decoded = await this.decodeToken();
if (typeof decoded.email_verified === 'undefined') {
throw new Error('No email verification found');
}
return decoded.email_verified as boolean;
}
async getName(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.name === 'undefined') {
return null;
}
return decoded.name as string;
}
async getPremium(): Promise<boolean> {
const decoded = await this.decodeToken();
if (typeof decoded.premium === 'undefined') {
return false;
}
return decoded.premium as boolean;
}
async getIssuer(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.iss === 'undefined') {
throw new Error('No issuer found');
}
return decoded.iss as string;
}
async getIsExternal(): Promise<boolean> {
const decoded = await this.decodeToken();
if (!Array.isArray(decoded.amr)) {
throw new Error('No amr found');
}
return decoded.amr.includes('external');
}
private async skipTokenStorage(): Promise<boolean> {
const timeout = await this.stateService.getVaultTimeout();
const action = await this.stateService.getVaultTimeoutAction();
return timeout != null && action === 'logOut';
}
private async skipTokenStorage(): Promise<boolean> {
const timeout = await this.stateService.getVaultTimeout();
const action = await this.stateService.getVaultTimeoutAction();
return timeout != null && action === "logOut";
}
}

View File

@@ -1,168 +1,178 @@
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { LogService } from '../abstractions/log.service';
import { StateService } from '../abstractions/state.service';
import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.service';
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { LogService } from "../abstractions/log.service";
import { StateService } from "../abstractions/state.service";
import { TotpService as TotpServiceAbstraction } from "../abstractions/totp.service";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
const B32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const SteamChars = '23456789BCDFGHJKMNPQRTVWXY';
const B32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const SteamChars = "23456789BCDFGHJKMNPQRTVWXY";
export class TotpService implements TotpServiceAbstraction {
constructor(private cryptoFunctionService: CryptoFunctionService, private logService: LogService,
private stateService: StateService) { }
constructor(
private cryptoFunctionService: CryptoFunctionService,
private logService: LogService,
private stateService: StateService
) {}
async getCode(key: string): Promise<string> {
if (key == null) {
return null;
async getCode(key: string): Promise<string> {
if (key == null) {
return null;
}
let period = 30;
let alg: "sha1" | "sha256" | "sha512" = "sha1";
let digits = 6;
let keyB32 = key;
const isOtpAuth = key.toLowerCase().indexOf("otpauth://") === 0;
const isSteamAuth = !isOtpAuth && key.toLowerCase().indexOf("steam://") === 0;
if (isOtpAuth) {
const params = Utils.getQueryParams(key);
if (params.has("digits") && params.get("digits") != null) {
try {
const digitParams = parseInt(params.get("digits").trim(), null);
if (digitParams > 10) {
digits = 10;
} else if (digitParams > 0) {
digits = digitParams;
}
} catch {
this.logService.error("Invalid digits param.");
}
let period = 30;
let alg: 'sha1' | 'sha256' | 'sha512' = 'sha1';
let digits = 6;
let keyB32 = key;
const isOtpAuth = key.toLowerCase().indexOf('otpauth://') === 0;
const isSteamAuth = !isOtpAuth && key.toLowerCase().indexOf('steam://') === 0;
if (isOtpAuth) {
const params = Utils.getQueryParams(key);
if (params.has('digits') && params.get('digits') != null) {
try {
const digitParams = parseInt(params.get('digits').trim(), null);
if (digitParams > 10) {
digits = 10;
} else if (digitParams > 0) {
digits = digitParams;
}
} catch {
this.logService.error('Invalid digits param.');
}
}
if (params.has('period') && params.get('period') != null) {
try {
const periodParam = parseInt(params.get('period').trim(), null);
if (periodParam > 0) {
period = periodParam;
}
} catch {
this.logService.error('Invalid period param.');
}
}
if (params.has('secret') && params.get('secret') != null) {
keyB32 = params.get('secret');
}
if (params.has('algorithm') && params.get('algorithm') != null) {
const algParam = params.get('algorithm').toLowerCase();
if (algParam === 'sha1' || algParam === 'sha256' || algParam === 'sha512') {
alg = algParam;
}
}
} else if (isSteamAuth) {
keyB32 = key.substr('steam://'.length);
digits = 5;
}
if (params.has("period") && params.get("period") != null) {
try {
const periodParam = parseInt(params.get("period").trim(), null);
if (periodParam > 0) {
period = periodParam;
}
} catch {
this.logService.error("Invalid period param.");
}
const epoch = Math.round(new Date().getTime() / 1000.0);
const timeHex = this.leftPad(this.decToHex(Math.floor(epoch / period)), 16, '0');
const timeBytes = Utils.fromHexToArray(timeHex);
const keyBytes = this.b32ToBytes(keyB32);
if (!keyBytes.length || !timeBytes.length) {
return null;
}
if (params.has("secret") && params.get("secret") != null) {
keyB32 = params.get("secret");
}
if (params.has("algorithm") && params.get("algorithm") != null) {
const algParam = params.get("algorithm").toLowerCase();
if (algParam === "sha1" || algParam === "sha256" || algParam === "sha512") {
alg = algParam;
}
const hash = await this.sign(keyBytes, timeBytes, alg);
if (hash.length === 0) {
return null;
}
/* tslint:disable */
const offset = (hash[hash.length - 1] & 0xf);
const binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
/* tslint:enable */
let otp = '';
if (isSteamAuth) {
// tslint:disable-next-line
let fullCode = binary & 0x7fffffff;
for (let i = 0; i < digits; i++) {
otp += SteamChars[fullCode % SteamChars.length];
fullCode = Math.trunc(fullCode / SteamChars.length);
}
} else {
otp = (binary % Math.pow(10, digits)).toString();
otp = this.leftPad(otp, digits, '0');
}
return otp;
}
} else if (isSteamAuth) {
keyB32 = key.substr("steam://".length);
digits = 5;
}
getTimeInterval(key: string): number {
let period = 30;
if (key != null && key.toLowerCase().indexOf('otpauth://') === 0) {
const params = Utils.getQueryParams(key);
if (params.has('period') && params.get('period') != null) {
try {
period = parseInt(params.get('period').trim(), null);
} catch {
this.logService.error('Invalid period param.');
}
}
const epoch = Math.round(new Date().getTime() / 1000.0);
const timeHex = this.leftPad(this.decToHex(Math.floor(epoch / period)), 16, "0");
const timeBytes = Utils.fromHexToArray(timeHex);
const keyBytes = this.b32ToBytes(keyB32);
if (!keyBytes.length || !timeBytes.length) {
return null;
}
const hash = await this.sign(keyBytes, timeBytes, alg);
if (hash.length === 0) {
return null;
}
/* tslint:disable */
const offset = hash[hash.length - 1] & 0xf;
const binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
/* tslint:enable */
let otp = "";
if (isSteamAuth) {
// tslint:disable-next-line
let fullCode = binary & 0x7fffffff;
for (let i = 0; i < digits; i++) {
otp += SteamChars[fullCode % SteamChars.length];
fullCode = Math.trunc(fullCode / SteamChars.length);
}
} else {
otp = (binary % Math.pow(10, digits)).toString();
otp = this.leftPad(otp, digits, "0");
}
return otp;
}
getTimeInterval(key: string): number {
let period = 30;
if (key != null && key.toLowerCase().indexOf("otpauth://") === 0) {
const params = Utils.getQueryParams(key);
if (params.has("period") && params.get("period") != null) {
try {
period = parseInt(params.get("period").trim(), null);
} catch {
this.logService.error("Invalid period param.");
}
return period;
}
}
return period;
}
async isAutoCopyEnabled(): Promise<boolean> {
return !(await this.stateService.getDisableAutoTotpCopy());
async isAutoCopyEnabled(): Promise<boolean> {
return !(await this.stateService.getDisableAutoTotpCopy());
}
// 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;
}
// Helpers
private decToHex(d: number): string {
return (d < 15.5 ? "0" : "") + Math.round(d).toString(16);
}
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 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;
private decToHex(d: number): string {
return (d < 15.5 ? '0' : '') + Math.round(d).toString(16);
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");
}
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;
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 Utils.fromHexToArray(this.b32ToHex(s));
}
private b32ToBytes(s: string): Uint8Array {
return Utils.fromHexToArray(this.b32ToHex(s));
}
private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array, alg: 'sha1' | 'sha256' | 'sha512') {
const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg);
return new Uint8Array(signature);
}
private async sign(
keyBytes: Uint8Array,
timeBytes: Uint8Array,
alg: "sha1" | "sha256" | "sha512"
) {
const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg);
return new Uint8Array(signature);
}
}

View File

@@ -1,69 +1,77 @@
import { UserVerificationService as UserVerificationServiceAbstraction } from '../abstractions/userVerification.service';
import { UserVerificationService as UserVerificationServiceAbstraction } from "../abstractions/userVerification.service";
import { ApiService } from '../abstractions/api.service';
import { CryptoService } from '../abstractions/crypto.service';
import { I18nService } from '../abstractions/i18n.service';
import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
import { VerificationType } from '../enums/verificationType';
import { VerificationType } from "../enums/verificationType";
import { VerifyOTPRequest } from '../models/request/account/verifyOTPRequest';
import { SecretVerificationRequest } from '../models/request/secretVerificationRequest';
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
import { Verification } from '../types/verification';
import { Verification } from "../types/verification";
export class UserVerificationService implements UserVerificationServiceAbstraction {
constructor(private cryptoService: CryptoService, private i18nService: I18nService,
private apiService: ApiService) { }
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private apiService: ApiService
) {}
async buildRequest<T extends SecretVerificationRequest>(verification: Verification,
requestClass?: new () => T, alreadyHashed?: boolean) {
this.validateInput(verification);
async buildRequest<T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean
) {
this.validateInput(verification);
const request = requestClass != null
? new requestClass()
: new SecretVerificationRequest() as T;
const request =
requestClass != null ? new requestClass() : (new SecretVerificationRequest() as T);
if (verification.type === VerificationType.OTP) {
request.otp = verification.secret;
} else {
request.masterPasswordHash = alreadyHashed
? verification.secret
: await this.cryptoService.hashPassword(verification.secret, null);
}
return request;
if (verification.type === VerificationType.OTP) {
request.otp = verification.secret;
} else {
request.masterPasswordHash = alreadyHashed
? verification.secret
: await this.cryptoService.hashPassword(verification.secret, null);
}
async verifyUser(verification: Verification): Promise<boolean> {
this.validateInput(verification);
return request;
}
if (verification.type === VerificationType.OTP) {
const request = new VerifyOTPRequest(verification.secret);
try {
await this.apiService.postAccountVerifyOTP(request);
} catch (e) {
throw new Error(this.i18nService.t('invalidVerificationCode'));
}
} else {
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(verification.secret, null);
if (!passwordValid) {
throw new Error(this.i18nService.t('invalidMasterPassword'));
}
}
return true;
}
async verifyUser(verification: Verification): Promise<boolean> {
this.validateInput(verification);
async requestOTP() {
await this.apiService.postAccountRequestOTP();
if (verification.type === VerificationType.OTP) {
const request = new VerifyOTPRequest(verification.secret);
try {
await this.apiService.postAccountVerifyOTP(request);
} catch (e) {
throw new Error(this.i18nService.t("invalidVerificationCode"));
}
} else {
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
verification.secret,
null
);
if (!passwordValid) {
throw new Error(this.i18nService.t("invalidMasterPassword"));
}
}
return true;
}
private validateInput(verification: Verification) {
if (verification?.secret == null || verification.secret === '') {
if (verification.type === VerificationType.OTP) {
throw new Error(this.i18nService.t('verificationCodeRequired'));
} else {
throw new Error(this.i18nService.t('masterPassRequired'));
}
}
async requestOTP() {
await this.apiService.postAccountRequestOTP();
}
private validateInput(verification: Verification) {
if (verification?.secret == null || verification.secret === "") {
if (verification.type === VerificationType.OTP) {
throw new Error(this.i18nService.t("verificationCodeRequired"));
} else {
throw new Error(this.i18nService.t("masterPassRequired"));
}
}
}
}

View File

@@ -1,199 +1,203 @@
import { CipherService } from '../abstractions/cipher.service';
import { CollectionService } from '../abstractions/collection.service';
import { CryptoService } from '../abstractions/crypto.service';
import { FolderService } from '../abstractions/folder.service';
import { KeyConnectorService } from '../abstractions/keyConnector.service';
import { MessagingService } from '../abstractions/messaging.service';
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
import { PolicyService } from '../abstractions/policy.service';
import { SearchService } from '../abstractions/search.service';
import { StateService } from '../abstractions/state.service';
import { TokenService } from '../abstractions/token.service';
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from '../abstractions/vaultTimeout.service';
import { KeySuffixOptions } from '../enums/keySuffixOptions';
import { CipherService } from "../abstractions/cipher.service";
import { CollectionService } from "../abstractions/collection.service";
import { CryptoService } from "../abstractions/crypto.service";
import { FolderService } from "../abstractions/folder.service";
import { KeyConnectorService } from "../abstractions/keyConnector.service";
import { MessagingService } from "../abstractions/messaging.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import { PolicyService } from "../abstractions/policy.service";
import { SearchService } from "../abstractions/search.service";
import { StateService } from "../abstractions/state.service";
import { TokenService } from "../abstractions/token.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vaultTimeout.service";
import { KeySuffixOptions } from "../enums/keySuffixOptions";
import { PolicyType } from '../enums/policyType';
import { PolicyType } from "../enums/policyType";
export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private inited = false;
private inited = false;
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private collectionService: CollectionService,
private cryptoService: CryptoService,
protected platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private searchService: SearchService,
private tokenService: TokenService,
private policyService: PolicyService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
private lockedCallback: () => Promise<void> = null,
private loggedOutCallback: (userId?: string) => Promise<void> = null
) {}
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private collectionService: CollectionService,
private cryptoService: CryptoService,
protected platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private searchService: SearchService,
private tokenService: TokenService,
private policyService: PolicyService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
private lockedCallback: () => Promise<void> = null,
private loggedOutCallback: (userId?: string) => Promise<void> = null
) {}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
this.inited = true;
if (checkOnInterval) {
this.startCheck();
}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
startCheck() {
this.checkVaultTimeout();
setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds
this.inited = true;
if (checkOnInterval) {
this.startCheck();
}
}
startCheck() {
this.checkVaultTimeout();
setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds
}
// Keys aren't stored for a device that is locked or logged out.
async isLocked(userId?: string): Promise<boolean> {
const neverLock =
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
if (neverLock) {
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
// We should refactor here.
await this.cryptoService.getKey(KeySuffixOptions.Auto, userId);
}
// Keys aren't stored for a device that is locked or logged out.
async isLocked(userId?: string): Promise<boolean> {
const neverLock = await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId) &&
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
if (neverLock) {
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
// We should refactor here.
await this.cryptoService.getKey(KeySuffixOptions.Auto, userId);
}
return !(await this.cryptoService.hasKeyInMemory(userId));
}
return !(await this.cryptoService.hasKeyInMemory(userId));
async checkVaultTimeout(): Promise<void> {
if (await this.platformUtilsService.isViewOpen()) {
return;
}
async checkVaultTimeout(): Promise<void> {
if (await this.platformUtilsService.isViewOpen()) {
return;
}
for (const userId in this.stateService.accounts.getValue()) {
if (userId != null && (await this.shouldLock(userId))) {
await this.executeTimeoutAction(userId);
}
}
}
for (const userId in this.stateService.accounts.getValue()) {
if (userId != null && await this.shouldLock(userId)) {
await this.executeTimeoutAction(userId);
}
}
async lock(allowSoftLock = false, userId?: string): Promise<void> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
async lock(allowSoftLock = false, userId?: string): Promise<void> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
if (await this.keyConnectorService.getUsesKeyConnector()) {
const pinSet = await this.isPinLockSet();
const pinLock =
(pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) || pinSet[1];
if (await this.keyConnectorService.getUsesKeyConnector()) {
const pinSet = await this.isPinLockSet();
const pinLock = (pinSet[0] && await this.stateService.getDecryptedPinProtected() != null) || pinSet[1];
if (!pinLock && !await this.isBiometricLockSet()) {
await this.logOut();
}
}
if (userId == null || userId === await this.stateService.getUserId()) {
this.searchService.clearIndex();
}
await this.stateService.setEverBeenUnlocked(true, { userId: userId });
await this.stateService.setBiometricLocked(true, { userId: userId });
await this.cryptoService.clearKey(false, userId);
await this.cryptoService.clearOrgKeys(true, userId);
await this.cryptoService.clearKeyPair(true, userId);
await this.cryptoService.clearEncKey(true, userId);
await this.folderService.clearCache(userId);
await this.cipherService.clearCache(userId);
await this.collectionService.clearCache(userId);
this.messagingService.send('locked', { userId: userId });
if (this.lockedCallback != null) {
await this.lockedCallback();
}
if (!pinLock && !(await this.isBiometricLockSet())) {
await this.logOut();
}
}
async logOut(userId?: string): Promise<void> {
if (this.loggedOutCallback != null) {
await this.loggedOutCallback(userId);
}
if (userId == null || userId === (await this.stateService.getUserId())) {
this.searchService.clearIndex();
}
async setVaultTimeoutOptions(timeout: number, action: string): Promise<void> {
await this.stateService.setVaultTimeout(timeout);
await this.stateService.setVaultTimeoutAction(action);
await this.cryptoService.toggleKey();
await this.tokenService.toggleTokens();
await this.stateService.setEverBeenUnlocked(true, { userId: userId });
await this.stateService.setBiometricLocked(true, { userId: userId });
await this.cryptoService.clearKey(false, userId);
await this.cryptoService.clearOrgKeys(true, userId);
await this.cryptoService.clearKeyPair(true, userId);
await this.cryptoService.clearEncKey(true, userId);
await this.folderService.clearCache(userId);
await this.cipherService.clearCache(userId);
await this.collectionService.clearCache(userId);
this.messagingService.send("locked", { userId: userId });
if (this.lockedCallback != null) {
await this.lockedCallback();
}
}
async logOut(userId?: string): Promise<void> {
if (this.loggedOutCallback != null) {
await this.loggedOutCallback(userId);
}
}
async setVaultTimeoutOptions(timeout: number, action: string): Promise<void> {
await this.stateService.setVaultTimeout(timeout);
await this.stateService.setVaultTimeoutAction(action);
await this.cryptoService.toggleKey();
await this.tokenService.toggleTokens();
}
async isPinLockSet(): Promise<[boolean, boolean]> {
const protectedPin = await this.stateService.getProtectedPin();
const pinProtectedKey = await this.stateService.getEncryptedPinProtected();
return [protectedPin != null, pinProtectedKey != null];
}
async isBiometricLockSet(): Promise<boolean> {
return await this.stateService.getBiometricUnlock();
}
async getVaultTimeout(userId?: string): Promise<number> {
const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId });
if (
await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)
) {
const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId);
// Remove negative values, and ensure it's smaller than maximum allowed value according to policy
let timeout = Math.min(vaultTimeout, policy[0].data.minutes);
if (vaultTimeout == null || timeout < 0) {
timeout = policy[0].data.minutes;
}
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (vaultTimeout !== timeout) {
await this.stateService.setVaultTimeout(timeout, { userId: userId });
}
return timeout;
}
async isPinLockSet(): Promise<[boolean, boolean]> {
const protectedPin = await this.stateService.getProtectedPin();
const pinProtectedKey = await this.stateService.getEncryptedPinProtected();
return [protectedPin != null, pinProtectedKey != null];
return vaultTimeout;
}
async clear(userId?: string): Promise<void> {
await this.stateService.setEverBeenUnlocked(false, { userId: userId });
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId });
}
private async isLoggedOut(userId?: string): Promise<boolean> {
return !(await this.stateService.getIsAuthenticated({ userId: userId }));
}
private async shouldLock(userId: string): Promise<boolean> {
if (await this.isLoggedOut(userId)) {
return false;
}
async isBiometricLockSet(): Promise<boolean> {
return await this.stateService.getBiometricUnlock();
if (await this.isLocked(userId)) {
return false;
}
async getVaultTimeout(userId?: string): Promise<number> {
const vaultTimeout = await this.stateService.getVaultTimeout( { userId: userId } );
if (await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)) {
const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId);
// Remove negative values, and ensure it's smaller than maximum allowed value according to policy
let timeout = Math.min(vaultTimeout, policy[0].data.minutes);
if (vaultTimeout == null || timeout < 0) {
timeout = policy[0].data.minutes;
}
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (vaultTimeout !== timeout) {
await this.stateService.setVaultTimeout(timeout, { userId: userId });
}
return timeout;
}
return vaultTimeout;
const vaultTimeout = await this.getVaultTimeout(userId);
if (vaultTimeout == null || vaultTimeout < 0) {
return false;
}
async clear(userId?: string): Promise<void> {
await this.stateService.setEverBeenUnlocked(false, { userId: userId });
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId });
const lastActive = await this.stateService.getLastActive({ userId: userId });
if (lastActive == null) {
return false;
}
private async isLoggedOut(userId?: string): Promise<boolean> {
return !(await this.stateService.getIsAuthenticated({ userId: userId }));
}
const vaultTimeoutSeconds = vaultTimeout * 60;
const diffSeconds = (new Date().getTime() - lastActive) / 1000;
return diffSeconds >= vaultTimeoutSeconds;
}
private async shouldLock(userId: string): Promise<boolean> {
if (await this.isLoggedOut(userId)) {
return false;
}
if (await this.isLocked(userId)) {
return false;
}
const vaultTimeout = await this.getVaultTimeout(userId);
if (vaultTimeout == null || vaultTimeout < 0) {
return false;
}
const lastActive = await this.stateService.getLastActive({ userId: userId });
if (lastActive == null) {
return false;
}
const vaultTimeoutSeconds = vaultTimeout * 60;
const diffSeconds = ((new Date()).getTime() - lastActive) / 1000;
return diffSeconds >= vaultTimeoutSeconds;
}
private async executeTimeoutAction(userId: string): Promise<void> {
const timeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId });
timeoutAction === 'logOut' ? await this.logOut() : await this.lock(true, userId);
}
private async executeTimeoutAction(userId: string): Promise<void> {
const timeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId });
timeoutAction === "logOut" ? await this.logOut() : await this.lock(true, userId);
}
}

View File

@@ -1,332 +1,389 @@
import * as forge from 'node-forge';
import * as forge from "node-forge";
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import { Utils } from '../misc/utils';
import { Utils } from "../misc/utils";
import { DecryptParameters } from '../models/domain/decryptParameters';
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
import { DecryptParameters } from "../models/domain/decryptParameters";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
export class WebCryptoFunctionService implements CryptoFunctionService {
private crypto: Crypto;
private subtle: SubtleCrypto;
private isIE: boolean;
private isOldSafari: boolean;
private crypto: Crypto;
private subtle: SubtleCrypto;
private isIE: boolean;
private isOldSafari: boolean;
constructor(private win: Window, private platformUtilsService: PlatformUtilsService) {
this.crypto = typeof win.crypto !== 'undefined' ? win.crypto : null;
this.subtle = (!!this.crypto && typeof win.crypto.subtle !== 'undefined') ? win.crypto.subtle : null;
this.isIE = platformUtilsService.isIE();
const ua = win.navigator.userAgent;
this.isOldSafari = platformUtilsService.isSafari() &&
(ua.indexOf(' Version/10.') > -1 || ua.indexOf(' Version/9.') > -1);
constructor(private win: Window, private platformUtilsService: PlatformUtilsService) {
this.crypto = typeof win.crypto !== "undefined" ? win.crypto : null;
this.subtle =
!!this.crypto && typeof win.crypto.subtle !== "undefined" ? win.crypto.subtle : null;
this.isIE = platformUtilsService.isIE();
const ua = win.navigator.userAgent;
this.isOldSafari =
platformUtilsService.isSafari() &&
(ua.indexOf(" Version/10.") > -1 || ua.indexOf(" Version/9.") > -1);
}
async pbkdf2(
password: string | ArrayBuffer,
salt: string | ArrayBuffer,
algorithm: "sha256" | "sha512",
iterations: number
): Promise<ArrayBuffer> {
if (this.isIE || this.isOldSafari) {
const forgeLen = algorithm === "sha256" ? 32 : 64;
const passwordBytes = this.toByteString(password);
const saltBytes = this.toByteString(salt);
const derivedKeyBytes = (forge as any).pbkdf2(
passwordBytes,
saltBytes,
iterations,
forgeLen,
algorithm
);
return Utils.fromByteStringToArray(derivedKeyBytes).buffer;
}
async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512',
iterations: number): Promise<ArrayBuffer> {
if (this.isIE || this.isOldSafari) {
const forgeLen = algorithm === 'sha256' ? 32 : 64;
const passwordBytes = this.toByteString(password);
const saltBytes = this.toByteString(salt);
const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, forgeLen, algorithm);
return Utils.fromByteStringToArray(derivedKeyBytes).buffer;
}
const wcLen = algorithm === "sha256" ? 256 : 512;
const passwordBuf = this.toBuf(password);
const saltBuf = this.toBuf(salt);
const wcLen = algorithm === 'sha256' ? 256 : 512;
const passwordBuf = this.toBuf(password);
const saltBuf = this.toBuf(salt);
const pbkdf2Params: Pbkdf2Params = {
name: "PBKDF2",
salt: saltBuf,
iterations: iterations,
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const pbkdf2Params: Pbkdf2Params = {
name: 'PBKDF2',
salt: saltBuf,
iterations: iterations,
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey(
"raw",
passwordBuf,
{ name: "PBKDF2" } as any,
false,
["deriveBits"]
);
return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen);
}
const impKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' } as any,
false, ['deriveBits']);
return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen);
async hkdf(
ikm: ArrayBuffer,
salt: string | ArrayBuffer,
info: string | ArrayBuffer,
outputByteSize: number,
algorithm: "sha256" | "sha512"
): Promise<ArrayBuffer> {
const saltBuf = this.toBuf(salt);
const infoBuf = this.toBuf(info);
const hkdfParams: HkdfParams = {
name: "HKDF",
salt: saltBuf,
info: infoBuf,
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey("raw", ikm, { name: "HKDF" } as any, false, [
"deriveBits",
]);
return await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8);
}
// ref: https://tools.ietf.org/html/rfc5869
async hkdfExpand(
prk: ArrayBuffer,
info: string | ArrayBuffer,
outputByteSize: number,
algorithm: "sha256" | "sha512"
): Promise<ArrayBuffer> {
const hashLen = algorithm === "sha256" ? 32 : 64;
if (outputByteSize > 255 * hashLen) {
throw new Error("outputByteSize is too large.");
}
const prkArr = new Uint8Array(prk);
if (prkArr.length < hashLen) {
throw new Error("prk is too small.");
}
const infoBuf = this.toBuf(info);
const infoArr = new Uint8Array(infoBuf);
let runningOkmLength = 0;
let previousT = new Uint8Array(0);
const n = Math.ceil(outputByteSize / hashLen);
const okm = new Uint8Array(n * hashLen);
for (let i = 0; i < n; i++) {
const t = new Uint8Array(previousT.length + infoArr.length + 1);
t.set(previousT);
t.set(infoArr, previousT.length);
t.set([i + 1], t.length - 1);
previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm));
okm.set(previousT, runningOkmLength);
runningOkmLength += previousT.length;
if (runningOkmLength >= outputByteSize) {
break;
}
}
return okm.slice(0, outputByteSize).buffer;
}
async hash(
value: string | ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512" | "md5"
): Promise<ArrayBuffer> {
if ((this.isIE && algorithm === "sha1") || algorithm === "md5") {
const md = algorithm === "md5" ? forge.md.md5.create() : forge.md.sha1.create();
const valueBytes = this.toByteString(value);
md.update(valueBytes, "raw");
return Utils.fromByteStringToArray(md.digest().data).buffer;
}
async hkdf(ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer,
outputByteSize: number, algorithm: 'sha256' | 'sha512'): Promise<ArrayBuffer> {
const saltBuf = this.toBuf(salt);
const infoBuf = this.toBuf(info);
const valueBuf = this.toBuf(value);
return await this.subtle.digest({ name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf);
}
const hkdfParams: HkdfParams = {
name: 'HKDF',
salt: saltBuf,
info: infoBuf,
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey('raw', ikm, { name: 'HKDF' } as any,
false, ['deriveBits']);
return await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8);
async hmac(
value: ArrayBuffer,
key: ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512"
): Promise<ArrayBuffer> {
if (this.isIE && algorithm === "sha512") {
const hmac = (forge as any).hmac.create();
const keyBytes = this.toByteString(key);
const valueBytes = this.toByteString(value);
hmac.start(algorithm, keyBytes);
hmac.update(valueBytes, "raw");
return Utils.fromByteStringToArray(hmac.digest().data).buffer;
}
// ref: https://tools.ietf.org/html/rfc5869
async hkdfExpand(prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number,
algorithm: 'sha256' | 'sha512'): Promise<ArrayBuffer> {
const hashLen = algorithm === 'sha256' ? 32 : 64;
if (outputByteSize > 255 * hashLen) {
throw new Error('outputByteSize is too large.');
}
const prkArr = new Uint8Array(prk);
if (prkArr.length < hashLen) {
throw new Error('prk is too small.');
}
const infoBuf = this.toBuf(info);
const infoArr = new Uint8Array(infoBuf);
let runningOkmLength = 0;
let previousT = new Uint8Array(0);
const n = Math.ceil(outputByteSize / hashLen);
const okm = new Uint8Array(n * hashLen);
for (let i = 0; i < n; i++) {
const t = new Uint8Array(previousT.length + infoArr.length + 1);
t.set(previousT);
t.set(infoArr, previousT.length);
t.set([i + 1], t.length - 1);
previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm));
okm.set(previousT, runningOkmLength);
runningOkmLength += previousT.length;
if (runningOkmLength >= outputByteSize) {
break;
}
}
return okm.slice(0, outputByteSize).buffer;
const signingAlgorithm = {
name: "HMAC",
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey("raw", key, signingAlgorithm, false, ["sign"]);
return await this.subtle.sign(signingAlgorithm, impKey, value);
}
// Safely compare two values in a way that protects against timing attacks (Double HMAC Verification).
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
async compare(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
const macKey = await this.randomBytes(32);
const signingAlgorithm = {
name: "HMAC",
hash: { name: "SHA-256" },
};
const impKey = await this.subtle.importKey("raw", macKey, signingAlgorithm, false, ["sign"]);
const mac1 = await this.subtle.sign(signingAlgorithm, impKey, a);
const mac2 = await this.subtle.sign(signingAlgorithm, impKey, b);
if (mac1.byteLength !== mac2.byteLength) {
return false;
}
async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise<ArrayBuffer> {
if ((this.isIE && algorithm === 'sha1') || algorithm === 'md5') {
const md = algorithm === 'md5' ? forge.md.md5.create() : forge.md.sha1.create();
const valueBytes = this.toByteString(value);
md.update(valueBytes, 'raw');
return Utils.fromByteStringToArray(md.digest().data).buffer;
}
const valueBuf = this.toBuf(value);
return await this.subtle.digest({ name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf);
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;
}
}
async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise<ArrayBuffer> {
if (this.isIE && algorithm === 'sha512') {
const hmac = (forge as any).hmac.create();
const keyBytes = this.toByteString(key);
const valueBytes = this.toByteString(value);
hmac.start(algorithm, keyBytes);
hmac.update(valueBytes, 'raw');
return Utils.fromByteStringToArray(hmac.digest().data).buffer;
}
return true;
}
const signingAlgorithm = {
name: 'HMAC',
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
hmacFast(value: string, key: string, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
const hmac = (forge as any).hmac.create();
hmac.start(algorithm, key);
hmac.update(value);
const bytes = hmac.digest().getBytes();
return Promise.resolve(bytes);
}
const impKey = await this.subtle.importKey('raw', key, signingAlgorithm, false, ['sign']);
return await this.subtle.sign(signingAlgorithm, impKey, value);
async compareFast(a: string, b: string): Promise<boolean> {
const rand = await this.randomBytes(32);
const bytes = new Uint32Array(rand);
const buffer = forge.util.createBuffer();
for (let i = 0; i < bytes.length; i++) {
buffer.putInt32(bytes[i]);
}
const macKey = buffer.getBytes();
const hmac = (forge as any).hmac.create();
hmac.start("sha256", macKey);
hmac.update(a);
const mac1 = hmac.digest().getBytes();
hmac.start(null, null);
hmac.update(b);
const mac2 = hmac.digest().getBytes();
const equals = mac1 === mac2;
return equals;
}
async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
"encrypt",
]);
return await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data);
}
aesDecryptFastParameters(
data: string,
iv: string,
mac: string,
key: SymmetricCryptoKey
): DecryptParameters<string> {
const p = new DecryptParameters<string>();
if (key.meta != null) {
p.encKey = key.meta.encKeyByteString;
p.macKey = key.meta.macKeyByteString;
}
// Safely compare two values in a way that protects against timing attacks (Double HMAC Verification).
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
async compare(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
const macKey = await this.randomBytes(32);
const signingAlgorithm = {
name: 'HMAC',
hash: { name: 'SHA-256' },
};
const impKey = await this.subtle.importKey('raw', macKey, signingAlgorithm, false, ['sign']);
const mac1 = await this.subtle.sign(signingAlgorithm, impKey, a);
const mac2 = await this.subtle.sign(signingAlgorithm, impKey, b);
if (mac1.byteLength !== mac2.byteLength) {
return false;
}
const arr1 = new Uint8Array(mac1);
const arr2 = new Uint8Array(mac2);
for (let i = 0; i < arr2.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
if (p.encKey == null) {
p.encKey = forge.util.decode64(key.encKeyB64);
}
p.data = forge.util.decode64(data);
p.iv = forge.util.decode64(iv);
p.macData = p.iv + p.data;
if (p.macKey == null && key.macKeyB64 != null) {
p.macKey = forge.util.decode64(key.macKeyB64);
}
if (mac != null) {
p.mac = forge.util.decode64(mac);
}
hmacFast(value: string, key: string, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise<string> {
const hmac = (forge as any).hmac.create();
hmac.start(algorithm, key);
hmac.update(value);
const bytes = hmac.digest().getBytes();
return Promise.resolve(bytes);
// cache byte string keys for later
if (key.meta == null) {
key.meta = {};
}
if (key.meta.encKeyByteString == null) {
key.meta.encKeyByteString = p.encKey;
}
if (p.macKey != null && key.meta.macKeyByteString == null) {
key.meta.macKeyByteString = p.macKey;
}
async compareFast(a: string, b: string): Promise<boolean> {
const rand = await this.randomBytes(32);
const bytes = new Uint32Array(rand);
const buffer = forge.util.createBuffer();
for (let i = 0; i < bytes.length; i++) {
buffer.putInt32(bytes[i]);
}
const macKey = buffer.getBytes();
return p;
}
const hmac = (forge as any).hmac.create();
hmac.start('sha256', macKey);
hmac.update(a);
const mac1 = hmac.digest().getBytes();
aesDecryptFast(parameters: DecryptParameters<string>): Promise<string> {
const dataBuffer = (forge as any).util.createBuffer(parameters.data);
const decipher = (forge as any).cipher.createDecipher("AES-CBC", parameters.encKey);
decipher.start({ iv: parameters.iv });
decipher.update(dataBuffer);
decipher.finish();
const val = decipher.output.toString("utf8");
return Promise.resolve(val);
}
hmac.start(null, null);
hmac.update(b);
const mac2 = hmac.digest().getBytes();
async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
"decrypt",
]);
return await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
}
const equals = mac1 === mac2;
return equals;
async rsaEncrypt(
data: ArrayBuffer,
publicKey: ArrayBuffer,
algorithm: "sha1" | "sha256"
): Promise<ArrayBuffer> {
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
// We cannot use the proper types here.
const rsaParams = {
name: "RSA-OAEP",
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey("spki", publicKey, rsaParams, false, ["encrypt"]);
return await this.subtle.encrypt(rsaParams, impKey, data);
}
async rsaDecrypt(
data: ArrayBuffer,
privateKey: ArrayBuffer,
algorithm: "sha1" | "sha256"
): Promise<ArrayBuffer> {
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
// We cannot use the proper types here.
const rsaParams = {
name: "RSA-OAEP",
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, false, ["decrypt"]);
return await this.subtle.decrypt(rsaParams, impKey, data);
}
async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise<ArrayBuffer> {
const rsaParams = {
name: "RSA-OAEP",
// Have to specify some algorithm
hash: { name: this.toWebCryptoAlgorithm("sha1") },
};
const impPrivateKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, true, [
"decrypt",
]);
const jwkPrivateKey = await this.subtle.exportKey("jwk", impPrivateKey);
const jwkPublicKeyParams = {
kty: "RSA",
e: jwkPrivateKey.e,
n: jwkPrivateKey.n,
alg: "RSA-OAEP",
ext: true,
};
const impPublicKey = await this.subtle.importKey("jwk", jwkPublicKeyParams, rsaParams, true, [
"encrypt",
]);
return await this.subtle.exportKey("spki", impPublicKey);
}
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> {
const rsaParams = {
name: "RSA-OAEP",
modulusLength: length,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
// Have to specify some algorithm
hash: { name: this.toWebCryptoAlgorithm("sha1") },
};
const keyPair = (await this.subtle.generateKey(rsaParams, true, [
"encrypt",
"decrypt",
])) as CryptoKeyPair;
const publicKey = await this.subtle.exportKey("spki", keyPair.publicKey);
const privateKey = await this.subtle.exportKey("pkcs8", keyPair.privateKey);
return [publicKey, privateKey];
}
randomBytes(length: number): Promise<ArrayBuffer> {
const arr = new Uint8Array(length);
this.crypto.getRandomValues(arr);
return Promise.resolve(arr.buffer);
}
private toBuf(value: string | ArrayBuffer): ArrayBuffer {
let buf: ArrayBuffer;
if (typeof value === "string") {
buf = Utils.fromUtf8ToArray(value).buffer;
} else {
buf = value;
}
return buf;
}
async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' } as any, false, ['encrypt']);
return await this.subtle.encrypt({ name: 'AES-CBC', iv: iv }, impKey, data);
private toByteString(value: string | ArrayBuffer): string {
let bytes: string;
if (typeof value === "string") {
bytes = forge.util.encodeUtf8(value);
} else {
bytes = Utils.fromBufferToByteString(value);
}
return bytes;
}
aesDecryptFastParameters(data: string, iv: string, mac: string, key: SymmetricCryptoKey):
DecryptParameters<string> {
const p = new DecryptParameters<string>();
if (key.meta != null) {
p.encKey = key.meta.encKeyByteString;
p.macKey = key.meta.macKeyByteString;
}
if (p.encKey == null) {
p.encKey = forge.util.decode64(key.encKeyB64);
}
p.data = forge.util.decode64(data);
p.iv = forge.util.decode64(iv);
p.macData = p.iv + p.data;
if (p.macKey == null && key.macKeyB64 != null) {
p.macKey = forge.util.decode64(key.macKeyB64);
}
if (mac != null) {
p.mac = forge.util.decode64(mac);
}
// cache byte string keys for later
if (key.meta == null) {
key.meta = {};
}
if (key.meta.encKeyByteString == null) {
key.meta.encKeyByteString = p.encKey;
}
if (p.macKey != null && key.meta.macKeyByteString == null) {
key.meta.macKeyByteString = p.macKey;
}
return p;
}
aesDecryptFast(parameters: DecryptParameters<string>): Promise<string> {
const dataBuffer = (forge as any).util.createBuffer(parameters.data);
const decipher = (forge as any).cipher.createDecipher('AES-CBC', parameters.encKey);
decipher.start({ iv: parameters.iv });
decipher.update(dataBuffer);
decipher.finish();
const val = decipher.output.toString('utf8');
return Promise.resolve(val);
}
async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' } as any, false, ['decrypt']);
return await this.subtle.decrypt({ name: 'AES-CBC', iv: iv }, impKey, data);
}
async rsaEncrypt(data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise<ArrayBuffer> {
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
// We cannot use the proper types here.
const rsaParams = {
name: 'RSA-OAEP',
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey('spki', publicKey, rsaParams, false, ['encrypt']);
return await this.subtle.encrypt(rsaParams, impKey, data);
}
async rsaDecrypt(data: ArrayBuffer, privateKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise<ArrayBuffer> {
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
// We cannot use the proper types here.
const rsaParams = {
name: 'RSA-OAEP',
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey('pkcs8', privateKey, rsaParams, false, ['decrypt']);
return await this.subtle.decrypt(rsaParams, impKey, data);
}
async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise<ArrayBuffer> {
const rsaParams = {
name: 'RSA-OAEP',
// Have to specify some algorithm
hash: { name: this.toWebCryptoAlgorithm('sha1') },
};
const impPrivateKey = await this.subtle.importKey('pkcs8', privateKey, rsaParams, true, ['decrypt']);
const jwkPrivateKey = await this.subtle.exportKey('jwk', impPrivateKey);
const jwkPublicKeyParams = {
kty: 'RSA',
e: jwkPrivateKey.e,
n: jwkPrivateKey.n,
alg: 'RSA-OAEP',
ext: true,
};
const impPublicKey = await this.subtle.importKey('jwk', jwkPublicKeyParams, rsaParams, true, ['encrypt']);
return await this.subtle.exportKey('spki', impPublicKey);
}
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> {
const rsaParams = {
name: 'RSA-OAEP',
modulusLength: length,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
// Have to specify some algorithm
hash: { name: this.toWebCryptoAlgorithm('sha1') },
};
const keyPair = (await this.subtle.generateKey(rsaParams, true, ['encrypt', 'decrypt'])) as CryptoKeyPair;
const publicKey = await this.subtle.exportKey('spki', keyPair.publicKey);
const privateKey = await this.subtle.exportKey('pkcs8', keyPair.privateKey);
return [publicKey, privateKey];
}
randomBytes(length: number): Promise<ArrayBuffer> {
const arr = new Uint8Array(length);
this.crypto.getRandomValues(arr);
return Promise.resolve(arr.buffer);
}
private toBuf(value: string | ArrayBuffer): ArrayBuffer {
let buf: ArrayBuffer;
if (typeof (value) === 'string') {
buf = Utils.fromUtf8ToArray(value).buffer;
} else {
buf = value;
}
return buf;
}
private toByteString(value: string | ArrayBuffer): string {
let bytes: string;
if (typeof (value) === 'string') {
bytes = forge.util.encodeUtf8(value);
} else {
bytes = Utils.fromBufferToByteString(value);
}
return bytes;
}
private toWebCryptoAlgorithm(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): string {
if (algorithm === 'md5') {
throw new Error('MD5 is not supported in WebCrypto.');
}
return algorithm === 'sha1' ? 'SHA-1' : algorithm === 'sha256' ? 'SHA-256' : 'SHA-512';
private toWebCryptoAlgorithm(algorithm: "sha1" | "sha256" | "sha512" | "md5"): string {
if (algorithm === "md5") {
throw new Error("MD5 is not supported in WebCrypto.");
}
return algorithm === "sha1" ? "SHA-1" : algorithm === "sha256" ? "SHA-256" : "SHA-512";
}
}