mirror of
https://github.com/bitwarden/browser
synced 2026-01-08 11:33:28 +00:00
Apply Prettier (#581)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user