1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-20 10:13:23 +00:00

[AC-3020] Remove unused jslib code - services (#575)

* Delete NotificationsService

* Remove SyncService

* Delete VaultTimeoutService

* Remove ProviderService

* Remove UserVerificationService

* Remove SendService

* Remove EventService

* Remove PasswordRepromptService

* Remove UsernameGenerationService

* Remove TotpService

* Remove CollectionService

* Remove FolderService

* Remove AuditService

* Remove CipherService and SearchService together

* Remove FileUploadService

* Remove SettingsService

* Remove SystemService

* Remove ElectronCryptoService

* Remove unused deps
This commit is contained in:
Thomas Rittson
2024-09-05 08:11:18 +10:00
committed by GitHub
parent 21cecc3c0a
commit eae9cac931
47 changed files with 13 additions and 5126 deletions

View File

@@ -1,6 +0,0 @@
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
export abstract class AuditService {
passwordLeaked: (password: string) => Promise<number>;
breachedAccounts: (username: string) => Promise<BreachAccountResponse[]>;
}

View File

@@ -1,79 +0,0 @@
import { CipherType } from "../enums/cipherType";
import { UriMatchType } from "../enums/uriMatchType";
import { CipherData } from "../models/data/cipherData";
import { Cipher } from "../models/domain/cipher";
import { Field } from "../models/domain/field";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { CipherView } from "../models/view/cipherView";
import { FieldView } from "../models/view/fieldView";
export abstract class CipherService {
clearCache: (userId?: string) => Promise<void>;
encrypt: (
model: CipherView,
key?: SymmetricCryptoKey,
originalCipher?: Cipher,
) => Promise<Cipher>;
encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise<Field[]>;
encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise<Field>;
get: (id: string) => Promise<Cipher>;
getAll: () => Promise<Cipher[]>;
getAllDecrypted: () => Promise<CipherView[]>;
getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise<CipherView[]>;
getAllDecryptedForUrl: (
url: string,
includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchType,
) => Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
getNextCipherForUrl: (url: string) => Promise<CipherView>;
updateLastUsedIndexForUrl: (url: string) => void;
updateLastUsedDate: (id: string) => Promise<void>;
updateLastLaunchedDate: (id: string) => Promise<void>;
saveNeverDomain: (domain: string) => Promise<void>;
saveWithServer: (cipher: Cipher) => Promise<any>;
shareWithServer: (
cipher: CipherView,
organizationId: string,
collectionIds: string[],
) => Promise<any>;
shareManyWithServer: (
ciphers: CipherView[],
organizationId: string,
collectionIds: string[],
) => Promise<any>;
saveAttachmentWithServer: (
cipher: Cipher,
unencryptedFile: any,
admin?: boolean,
) => Promise<Cipher>;
saveAttachmentRawWithServer: (
cipher: Cipher,
filename: string,
data: ArrayBuffer,
admin?: boolean,
) => Promise<Cipher>;
saveCollectionsWithServer: (cipher: Cipher) => Promise<any>;
upsert: (cipher: CipherData | CipherData[]) => Promise<any>;
replace: (ciphers: { [id: string]: CipherData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
moveManyWithServer: (ids: string[], folderId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
deleteWithServer: (id: string) => Promise<any>;
deleteManyWithServer: (ids: string[]) => Promise<any>;
deleteAttachment: (id: string, attachmentId: string) => Promise<void>;
deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise<void>;
sortCiphersByLastUsed: (a: any, b: any) => number;
sortCiphersByLastUsedThenName: (a: any, b: any) => number;
getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number;
softDelete: (id: string | string[]) => Promise<any>;
softDeleteWithServer: (id: string) => Promise<any>;
softDeleteManyWithServer: (ids: string[]) => Promise<any>;
restore: (
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) => Promise<any>;
restoreWithServer: (id: string) => Promise<any>;
restoreManyWithServer: (ids: string[]) => Promise<any>;
}

View File

@@ -1,19 +0,0 @@
import { CollectionData } from "../models/data/collectionData";
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/treeNode";
import { CollectionView } from "../models/view/collectionView";
export abstract class CollectionService {
clearCache: (userId?: string) => Promise<void>;
encrypt: (model: CollectionView) => Promise<Collection>;
decryptMany: (collections: Collection[]) => Promise<CollectionView[]>;
get: (id: string) => Promise<Collection>;
getAll: () => Promise<Collection[]>;
getAllDecrypted: () => Promise<CollectionView[]>;
getAllNested: (collections?: CollectionView[]) => Promise<TreeNode<CollectionView>[]>;
getNested: (id: string) => Promise<TreeNode<CollectionView>>;
upsert: (collection: CollectionData | CollectionData[]) => Promise<any>;
replace: (collections: { [id: string]: CollectionData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@@ -1,7 +0,0 @@
import { EventType } from "../enums/eventType";
export abstract class EventService {
collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise<any>;
uploadEvents: (userId?: string) => Promise<any>;
clearEvents: (userId?: string) => Promise<any>;
}

View File

@@ -1,18 +0,0 @@
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { EncString } from "../models/domain/encString";
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
import { SendFileUploadDataResponse } from "../models/response/sendFileUploadDataResponse";
export abstract class FileUploadService {
uploadSendFile: (
uploadData: SendFileUploadDataResponse,
fileName: EncString,
encryptedFileData: EncArrayBuffer,
) => Promise<any>;
uploadCipherAttachment: (
admin: boolean,
uploadData: AttachmentUploadDataResponse,
fileName: EncString,
encryptedFileData: EncArrayBuffer,
) => Promise<any>;
}

View File

@@ -1,21 +0,0 @@
import { FolderData } from "../models/data/folderData";
import { Folder } from "../models/domain/folder";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TreeNode } from "../models/domain/treeNode";
import { FolderView } from "../models/view/folderView";
export abstract class FolderService {
clearCache: (userId?: string) => Promise<void>;
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
get: (id: string) => Promise<Folder>;
getAll: () => Promise<Folder[]>;
getAllDecrypted: () => Promise<FolderView[]>;
getAllNested: () => Promise<TreeNode<FolderView>[]>;
getNested: (id: string) => Promise<TreeNode<FolderView>>;
saveWithServer: (folder: Folder) => Promise<any>;
upsert: (folder: FolderData | FolderData[]) => Promise<any>;
replace: (folders: { [id: string]: FolderData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
deleteWithServer: (id: string) => Promise<any>;
}

View File

@@ -1,6 +0,0 @@
export abstract class NotificationsService {
init: () => Promise<void>;
updateConnection: (sync?: boolean) => Promise<void>;
reconnectFromActivity: () => Promise<void>;
disconnectFromInactivity: () => Promise<void>;
}

View File

@@ -1,5 +0,0 @@
export abstract class PasswordRepromptService {
protectedFields: () => string[];
showPasswordPrompt: () => Promise<boolean>;
enabled: () => Promise<boolean>;
}

View File

@@ -1,8 +0,0 @@
import { ProviderData } from "../models/data/providerData";
import { Provider } from "../models/domain/provider";
export abstract class ProviderService {
get: (id: string) => Promise<Provider>;
getAll: () => Promise<Provider[]>;
save: (providers: { [id: string]: ProviderData }) => Promise<any>;
}

View File

@@ -1,16 +0,0 @@
import { CipherView } from "../models/view/cipherView";
import { SendView } from "../models/view/sendView";
export abstract class SearchService {
indexedEntityId?: string = null;
clearIndex: () => void;
isSearchable: (query: string) => boolean;
indexCiphers: (indexedEntityGuid?: string, ciphersToIndex?: CipherView[]) => Promise<void>;
searchCiphers: (
query: string,
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
ciphers?: CipherView[],
) => Promise<CipherView[]>;
searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[];
searchSends: (sends: SendView[], query: string) => SendView[];
}

View File

@@ -1,25 +0,0 @@
import { SendData } from "../models/data/sendData";
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { Send } from "../models/domain/send";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { SendView } from "../models/view/sendView";
export abstract class SendService {
clearCache: () => Promise<void>;
encrypt: (
model: SendView,
file: File | ArrayBuffer,
password: string,
key?: SymmetricCryptoKey,
) => Promise<[Send, EncArrayBuffer]>;
get: (id: string) => Promise<Send>;
getAll: () => Promise<Send[]>;
getAllDecrypted: () => Promise<SendView[]>;
saveWithServer: (sendData: [Send, EncArrayBuffer]) => Promise<any>;
upsert: (send: SendData | SendData[]) => Promise<any>;
replace: (sends: { [id: string]: SendData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
deleteWithServer: (id: string) => Promise<any>;
removePasswordWithServer: (id: string) => Promise<any>;
}

View File

@@ -1,6 +0,0 @@
export abstract class SettingsService {
clearCache: () => Promise<void>;
getEquivalentDomains: () => Promise<any>;
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -1,19 +0,0 @@
import {
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
} from "../models/response/notificationResponse";
export abstract class SyncService {
syncInProgress: boolean;
getLastSync: () => Promise<Date>;
setLastSync: (date: Date, userId?: string) => Promise<any>;
fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>;
syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise<boolean>;
syncDeleteFolder: (notification: SyncFolderNotification) => Promise<boolean>;
syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise<boolean>;
syncDeleteCipher: (notification: SyncFolderNotification) => Promise<boolean>;
syncUpsertSend: (notification: SyncSendNotification, isEdit: boolean) => Promise<boolean>;
syncDeleteSend: (notification: SyncSendNotification) => Promise<boolean>;
}

View File

@@ -1,6 +0,0 @@
export abstract class SystemService {
startProcessReload: () => Promise<void>;
cancelProcessReload: () => void;
clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise<void>;
clearPendingClipboard: () => Promise<any>;
}

View File

@@ -1,5 +0,0 @@
export abstract class TotpService {
getCode: (key: string) => Promise<string>;
getTimeInterval: (key: string) => number;
isAutoCopyEnabled: () => Promise<boolean>;
}

View File

@@ -1,12 +0,0 @@
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
import { Verification } from "../types/verification";
export abstract class UserVerificationService {
buildRequest: <T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean,
) => Promise<T>;
verifyUser: (verification: Verification) => Promise<boolean>;
requestOTP: () => Promise<void>;
}

View File

@@ -1,8 +0,0 @@
export abstract class UsernameGenerationService {
generateUsername: (options: any) => Promise<string>;
generateWord: (options: any) => Promise<string>;
generateSubaddress: (options: any) => Promise<string>;
generateCatchall: (options: any) => Promise<string>;
getOptions: () => Promise<any>;
saveOptions: (options: any) => Promise<void>;
}

View File

@@ -1,11 +0,0 @@
export abstract class VaultTimeoutService {
isLocked: (userId?: string) => Promise<boolean>;
checkVaultTimeout: () => Promise<void>;
lock: (allowSoftLock?: boolean, userId?: string) => Promise<void>;
logOut: (userId?: string) => Promise<void>;
setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise<void>;
getVaultTimeout: () => Promise<number>;
isPinLockSet: () => Promise<[boolean, boolean]>;
isBiometricLockSet: () => Promise<boolean>;
clear: (userId?: string) => Promise<any>;
}

View File

@@ -1,44 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "../abstractions/audit.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { throttle } from "../misc/throttle";
import { Utils } from "../misc/utils";
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
import { ErrorResponse } from "../models/response/errorResponse";
const PwnedPasswordsApi = "https://api.pwnedpasswords.com/range/";
export class AuditService implements AuditServiceAbstraction {
constructor(
private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
) {}
@throttle(100, () => "passwordLeaked")
async passwordLeaked(password: string): Promise<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;
});
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();
}
}
}

View File

@@ -1,214 +0,0 @@
import { LogService } from "../abstractions/log.service";
import { Utils } from "../misc/utils";
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) {}
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",
});
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}`,
);
}
// eslint-disable-next-line
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 blockRequest = new Request(blockUrl.toString(), {
body: blockData,
cache: "no-store",
method: "PUT",
headers: blockHeaders,
});
const blockResponse = await fetch(blockRequest);
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);
}
blocksStaged.push(blockId);
blockIndex++;
}
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 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;
}
}
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;
}
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 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;
}
year = 0;
month = 0;
day = 0;
constructor(version: string) {
try {
const parts = version.split("-").map((v) => Number.parseInt(v, 10));
this.year = parts[0];
this.month = parts[1];
this.day = parts[2];
} catch {
// Ignore error
}
}
/**
* Compares two Azure Versions against each other
* @param compareTo Version to compare against
* @returns a number less than zero if compareTo is newer, 0 if equal,
* and greater than zero if this is greater than compareTo
*/
compare(compareTo: Required<Version> | string) {
return Version.compare(this, compareTo);
}
}

View File

@@ -1,34 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { Utils } from "../misc/utils";
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,157 +0,0 @@
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 { CollectionData } from "../models/data/collectionData";
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/treeNode";
import { CollectionView } from "../models/view/collectionView";
const NestingDelimiter = "/";
export class CollectionService implements CollectionServiceAbstraction {
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();
// eslint-disable-next-line
if (collections == null || !collections.hasOwnProperty(id)) {
return null;
}
return new Collection(collections[id]);
}
async getAll(): Promise<Collection[]> {
const collections = await this.stateService.getEncryptedCollections();
const response: Collection[] = [];
for (const id in collections) {
// eslint-disable-next-line
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;
}
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;
}
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);
}
}

View File

@@ -1,99 +0,0 @@
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 { EventType } from "../enums/eventType";
import { EventData } from "../models/data/eventData";
import { EventRequest } from "../models/request/eventRequest";
export class EventService implements EventServiceAbstraction {
private inited = false;
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
}
}
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 uploadEvents(userId?: string): Promise<any> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
if (eventCollection == null || eventCollection.length === 0) {
return;
}
const request = eventCollection.map((e) => {
const req = new EventRequest();
req.type = e.type;
req.cipherId = e.cipherId;
req.date = e.date;
return req;
});
try {
await this.apiService.postEventsCollect(request);
this.clearEvents(userId);
} catch (e) {
this.logService.error(e);
}
}
async clearEvents(userId?: string): Promise<any> {
await this.stateService.setEventCollection(null, { userId: userId });
}
}

View File

@@ -1,111 +0,0 @@
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 { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { EncString } from "../models/domain/encString";
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
import { SendFileUploadDataResponse } from "../models/response/sendFileUploadDataResponse";
import { AzureFileUploadService } from "./azureFileUpload.service";
import { BitwardenFileUploadService } from "./bitwardenFileUpload.service";
export class FileUploadService implements FileUploadServiceAbstraction {
private azureFileUploadService: AzureFileUploadService;
private bitwardenFileUploadService: BitwardenFileUploadService;
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 uploadCipherAttachment(
admin: boolean,
uploadData: AttachmentUploadDataResponse,
encryptedFileName: EncString,
encryptedFileData: EncArrayBuffer,
) {
const response = admin ? uploadData.cipherMiniResponse : uploadData.cipherResponse;
try {
switch (uploadData.fileUploadType) {
case FileUploadType.Direct:
await this.bitwardenFileUploadService.upload(
encryptedFileName.encryptedString,
encryptedFileData,
(fd) => this.apiService.postAttachmentFile(response.id, uploadData.attachmentId, fd),
);
break;
case FileUploadType.Azure: {
const renewalCallback = async () => {
const renewalResponse = await this.apiService.renewAttachmentUploadUrl(
response.id,
uploadData.attachmentId,
);
return renewalResponse.url;
};
await this.azureFileUploadService.upload(
uploadData.url,
encryptedFileData,
renewalCallback,
);
break;
}
default:
throw new Error("Unknown file upload type.");
}
} catch (e) {
if (admin) {
await this.apiService.deleteCipherAttachmentAdmin(response.id, uploadData.attachmentId);
} else {
await this.apiService.deleteCipherAttachment(response.id, uploadData.attachmentId);
}
throw e;
}
}
}

View File

@@ -1,194 +0,0 @@
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 { ServiceUtils } from "../misc/serviceUtils";
import { Utils } from "../misc/utils";
import { CipherData } from "../models/data/cipherData";
import { FolderData } from "../models/data/folderData";
import { Folder } from "../models/domain/folder";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TreeNode } from "../models/domain/treeNode";
import { FolderRequest } from "../models/request/folderRequest";
import { FolderResponse } from "../models/response/folderResponse";
import { FolderView } from "../models/view/folderView";
const NestingDelimiter = "/";
export class FolderService implements FolderServiceAbstraction {
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 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();
// eslint-disable-next-line
if (folders == null || !folders.hasOwnProperty(id)) {
return null;
}
return new Folder(folders[id]);
}
async getAll(): Promise<Folder[]> {
const folders = await this.stateService.getEncryptedFolders();
const response: Folder[] = [];
for (const id in folders) {
// eslint-disable-next-line
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);
}
}

View File

@@ -1,231 +0,0 @@
import * as signalR from "@microsoft/signalr";
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
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 { NotificationType } from "../enums/notificationType";
import {
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;
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();
});
}
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)),
);
// eslint-disable-next-line
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;
}
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);
break;
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;
}
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));
}
}
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;
}
}

View File

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

View File

@@ -1,284 +0,0 @@
import * as lunr from "lunr";
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 { CipherView } from "../models/view/cipherView";
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;
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;
}
if (ciphers == null) {
ciphers = await this.cipherService.getAllDecrypted();
}
if (filter != null && Array.isArray(filter) && filter.length > 0) {
ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c)));
} else if (filter != null) {
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
}
if (!this.isSearchable(query)) {
return ciphers;
}
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 {
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, {});
});
});
}
if (searchResults != null) {
searchResults.forEach((r) => {
if (ciphersMap.has(r.ref)) {
results.push(ciphersMap.get(r.ref));
}
});
}
return results;
}
searchCiphersBasic(ciphers: CipherView[], query: string, deleted = 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;
}
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;
}
}

View File

@@ -1,297 +0,0 @@
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 { SEND_KDF_ITERATIONS } from "../enums/kdfType";
import { SendType } from "../enums/sendType";
import { Utils } from "../misc/utils";
import { SendData } from "../models/data/sendData";
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 { SendRequest } from "../models/request/sendRequest";
import { ErrorResponse } from "../models/response/errorResponse";
import { SendResponse } from "../models/response/sendResponse";
import { SendView } from "../models/view/sendView";
export class SendService implements SendServiceAbstraction {
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 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",
SEND_KDF_ITERATIONS,
);
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];
}
async get(id: string): Promise<Send> {
const sends = await this.stateService.getEncryptedSends();
// eslint-disable-next-line
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) {
// eslint-disable-next-line
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;
} else {
response = await this.apiService.putSend(sendData[0].id, request);
}
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.");
};
});
}
private async encryptFileData(
fileName: string,
data: ArrayBuffer,
key: SymmetricCryptoKey,
): Promise<[EncString, EncArrayBuffer]> {
const encFileName = await this.cryptoService.encrypt(fileName, key);
const encFileData = await this.cryptoService.encryptToBytes(data, key);
return [encFileName, encFileData];
}
}

View File

@@ -1,56 +0,0 @@
import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service";
import { StateService } from "../abstractions/state.service";
const Keys = {
settingsPrefix: "settings_",
equivalentDomains: "equivalentDomains",
};
export class SettingsService implements SettingsServiceAbstraction {
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) {
// eslint-disable-next-line
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);
}
}

View File

@@ -1,400 +0,0 @@
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 { sequentialize } from "../misc/sequentialize";
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 {
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 = 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>,
) {}
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 setLastSync(date: Date, userId?: string): Promise<any> {
await this.stateService.setLastSync(date.toJSON(), { userId: userId });
}
@sequentialize(() => "fullSync")
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
this.syncStarted();
const isAuthenticated = await this.stateService.getIsAuthenticated();
if (!isAuthenticated) {
return this.syncCompleted(false);
}
const now = new Date();
let needsSync = false;
try {
needsSync = await this.needsSyncing(forceSync);
} catch (e) {
if (allowThrowOnError) {
throw e;
}
}
if (!needsSync) {
await this.setLastSync(now);
return this.syncCompleted(false);
}
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) {
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 {
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 });
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);
}
}
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;
}
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);
}
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 this.organizationService.save(organizations);
await this.providerService.save(providers);
if (await this.keyConnectorService.userNeedsMigration()) {
await this.keyConnectorService.setConvertAccountRequired(true);
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);
}
}

View File

@@ -1,90 +0,0 @@
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";
export class SystemService implements SystemServiceAbstraction {
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,
) {}
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;
}
}
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;
}
}
}

View File

@@ -1,174 +0,0 @@
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";
const B32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const SteamChars = "23456789BCDFGHJKMNPQRTVWXY";
export class TotpService implements TotpServiceAbstraction {
constructor(
private cryptoFunctionService: CryptoFunctionService,
private logService: LogService,
private stateService: StateService,
) {}
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.");
}
}
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;
}
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;
}
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);
let otp = "";
if (isSteamAuth) {
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;
}
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;
}
private decToHex(d: number): string {
return (d < 15.5 ? "0" : "") + Math.round(d).toString(16);
}
private b32ToHex(s: string): string {
s = s.toUpperCase();
let cleanedInput = "";
for (let i = 0; i < s.length; i++) {
if (B32Chars.indexOf(s[i]) < 0) {
continue;
}
cleanedInput += s[i];
}
s = cleanedInput;
let bits = "";
let hex = "";
for (let i = 0; i < s.length; i++) {
const byteIndex = B32Chars.indexOf(s.charAt(i));
if (byteIndex < 0) {
continue;
}
bits += this.leftPad(byteIndex.toString(2), 5, "0");
}
for (let i = 0; i + 4 <= bits.length; i += 4) {
const chunk = bits.substr(i, 4);
hex = hex + parseInt(chunk, 2).toString(16);
}
return hex;
}
private b32ToBytes(s: string): Uint8Array {
return 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);
}
}

View File

@@ -1,88 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../abstractions/userVerification.service";
import { VerificationType } from "../enums/verificationType";
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
import { Verification } from "../types/verification";
/**
* Used for general-purpose user verification throughout the app.
* Use it to verify the input collected by UserVerificationComponent.
*/
export class UserVerificationService implements UserVerificationServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private apiService: ApiService,
) {}
/**
* Create a new request model to be used for server-side verification
* @param verification User-supplied verification data (Master Password or OTP)
* @param requestClass The request model to create
* @param alreadyHashed Whether the master password is already hashed
*/
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);
if (verification.type === VerificationType.OTP) {
request.otp = verification.secret;
} else {
request.masterPasswordHash = alreadyHashed
? verification.secret
: await this.cryptoService.hashPassword(verification.secret, null);
}
return request;
}
/**
* Used to verify the Master Password client-side, or send the OTP to the server for verification (with no other data)
* Generally used for client-side verification only.
* @param verification User-supplied verification data (Master Password or OTP)
*/
async verifyUser(verification: Verification): Promise<boolean> {
this.validateInput(verification);
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 requestOTP() {
await this.apiService.postAccountRequestOTP();
}
private validateInput(verification: Verification) {
if (verification?.secret == null || verification.secret === "") {
if (verification.type === VerificationType.OTP) {
throw new Error(this.i18nService.t("verificationCodeRequired"));
} else {
throw new Error(this.i18nService.t("masterPassRequired"));
}
}
}
}

View File

@@ -1,131 +0,0 @@
import { CryptoService } from "../abstractions/crypto.service";
import { StateService } from "../abstractions/state.service";
import { UsernameGenerationService as BaseUsernameGenerationService } from "../abstractions/usernameGeneration.service";
import { EEFLongWordList } from "../misc/wordlist";
const DefaultOptions = {
type: "word",
wordCapitalize: true,
wordIncludeNumber: true,
subaddressType: "random",
catchallType: "random",
};
export class UsernameGenerationService implements BaseUsernameGenerationService {
constructor(
private cryptoService: CryptoService,
private stateService: StateService,
) {}
generateUsername(options: any): Promise<string> {
if (options.type === "catchall") {
return this.generateCatchall(options);
} else if (options.type === "subaddress") {
return this.generateSubaddress(options);
} else if (options.type === "forwarded") {
return this.generateSubaddress(options);
} else {
return this.generateWord(options);
}
}
async generateWord(options: any): Promise<string> {
const o = Object.assign({}, DefaultOptions, options);
if (o.wordCapitalize == null) {
o.wordCapitalize = true;
}
if (o.wordIncludeNumber == null) {
o.wordIncludeNumber = true;
}
const wordIndex = await this.cryptoService.randomNumber(0, EEFLongWordList.length - 1);
let word = EEFLongWordList[wordIndex];
if (o.wordCapitalize) {
word = word.charAt(0).toUpperCase() + word.slice(1);
}
if (o.wordIncludeNumber) {
const num = await this.cryptoService.randomNumber(1, 9999);
word = word + this.zeroPad(num.toString(), 4);
}
return word;
}
async generateSubaddress(options: any): Promise<string> {
const o = Object.assign({}, DefaultOptions, options);
const subaddressEmail = o.subaddressEmail;
if (subaddressEmail == null || subaddressEmail.length < 3) {
return o.subaddressEmail;
}
const atIndex = subaddressEmail.indexOf("@");
if (atIndex < 1 || atIndex >= subaddressEmail.length - 1) {
return subaddressEmail;
}
if (o.subaddressType == null) {
o.subaddressType = "random";
}
const emailBeginning = subaddressEmail.substr(0, atIndex);
const emailEnding = subaddressEmail.substr(atIndex + 1, subaddressEmail.length);
let subaddressString = "";
if (o.subaddressType === "random") {
subaddressString = await this.randomString(8);
} else if (o.subaddressType === "website-name") {
subaddressString = o.website;
}
return emailBeginning + "+" + subaddressString + "@" + emailEnding;
}
async generateCatchall(options: any): Promise<string> {
const o = Object.assign({}, DefaultOptions, options);
if (o.catchallDomain == null || o.catchallDomain === "") {
return null;
}
if (o.catchallType == null) {
o.catchallType = "random";
}
let startString = "";
if (o.catchallType === "random") {
startString = await this.randomString(8);
} else if (o.catchallType === "website-name") {
startString = o.website;
}
return startString + "@" + o.catchallDomain;
}
async getOptions(): Promise<any> {
let options = await this.stateService.getUsernameGenerationOptions();
if (options == null) {
options = Object.assign({}, DefaultOptions);
} else {
options = Object.assign({}, DefaultOptions, options);
}
await this.stateService.setUsernameGenerationOptions(options);
return options;
}
async saveOptions(options: any) {
await this.stateService.setUsernameGenerationOptions(options);
}
private async randomString(length: number) {
let str = "";
const charSet = "abcdefghijklmnopqrstuvwxyz1234567890";
for (let i = 0; i < length; i++) {
const randomCharIndex = await this.cryptoService.randomNumber(0, charSet.length - 1);
str += charSet.charAt(randomCharIndex);
}
return str;
}
// ref: https://stackoverflow.com/a/10073788
private zeroPad(number: string, width: number) {
return number.length >= width
? number
: new Array(width - number.length + 1).join("0") + number;
}
}

View File

@@ -1,225 +0,0 @@
import { firstValueFrom } from "rxjs";
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";
export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
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: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (userId?: string) => Promise<void> = null,
) {}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
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);
}
return !(await this.cryptoService.hasKeyInMemory(userId));
}
async checkVaultTimeout(): Promise<void> {
if (await this.platformUtilsService.isViewOpen()) {
return;
}
const accounts = await firstValueFrom(this.stateService.accounts$);
for (const userId in accounts) {
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;
}
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(userId);
}
}
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.stateService.setCryptoMasterKeyAuto(null, { 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(userId);
}
}
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);
// We swap these tokens from being on disk for lock actions, and in memory for logout actions
// Get them here to set them to their new location after changing the timeout action and clearing if needed
const token = await this.tokenService.getToken();
const refreshToken = await this.tokenService.getRefreshToken();
const clientId = await this.tokenService.getClientId();
const clientSecret = await this.tokenService.getClientSecret();
const currentAction = await this.stateService.getVaultTimeoutAction();
if ((timeout != null || timeout === 0) && action === "logOut" && action !== currentAction) {
// if we have a vault timeout and the action is log out, reset tokens
await this.tokenService.clearToken();
}
await this.stateService.setVaultTimeoutAction(action);
await this.tokenService.setToken(token);
await this.tokenService.setRefreshToken(refreshToken);
await this.tokenService.setClientId(clientId);
await this.tokenService.setClientSecret(clientSecret);
await this.cryptoService.toggleKey();
}
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;
}
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;
}
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(userId) : await this.lock(true, userId);
}
}