mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 02:03:39 +00:00
Move to libs
This commit is contained in:
2626
libs/common/src/services/api.service.ts
Normal file
2626
libs/common/src/services/api.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
31
libs/common/src/services/appId.service.ts
Normal file
31
libs/common/src/services/appId.service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/appId.service";
|
||||
import { StorageService } from "../abstractions/storage.service";
|
||||
import { HtmlStorageLocation } from "../enums/htmlStorageLocation";
|
||||
import { Utils } from "../misc/utils";
|
||||
|
||||
export class AppIdService implements AppIdServiceAbstraction {
|
||||
constructor(private storageService: StorageService) {}
|
||||
|
||||
getAppId(): Promise<string> {
|
||||
return this.makeAndGetAppId("appId");
|
||||
}
|
||||
|
||||
getAnonymousAppId(): Promise<string> {
|
||||
return this.makeAndGetAppId("anonymousAppId");
|
||||
}
|
||||
|
||||
private async makeAndGetAppId(key: string) {
|
||||
const existingId = await this.storageService.get<string>(key, {
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
});
|
||||
if (existingId != null) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
const guid = Utils.newGuid();
|
||||
await this.storageService.save(key, guid, {
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
});
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
44
libs/common/src/services/audit.service.ts
Normal file
44
libs/common/src/services/audit.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
225
libs/common/src/services/auth.service.ts
Normal file
225
libs/common/src/services/auth.service.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { EnvironmentService } from "../abstractions/environment.service";
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
import { KeyConnectorService } from "../abstractions/keyConnector.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { MessagingService } from "../abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/twoFactor.service";
|
||||
import { AuthenticationStatus } from "../enums/authenticationStatus";
|
||||
import { AuthenticationType } from "../enums/authenticationType";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { KeySuffixOptions } from "../enums/keySuffixOptions";
|
||||
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
|
||||
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
|
||||
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
||||
import { AuthResult } from "../models/domain/authResult";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "../models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
|
||||
import { PreloginRequest } from "../models/request/preloginRequest";
|
||||
import { ErrorResponse } from "../models/response/errorResponse";
|
||||
|
||||
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
export class AuthService implements AuthServiceAbstraction {
|
||||
get email(): string {
|
||||
return this.logInStrategy instanceof PasswordLogInStrategy ? this.logInStrategy.email : null;
|
||||
}
|
||||
|
||||
get masterPasswordHash(): string {
|
||||
return this.logInStrategy instanceof PasswordLogInStrategy
|
||||
? this.logInStrategy.masterPasswordHash
|
||||
: null;
|
||||
}
|
||||
|
||||
private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
||||
private sessionTimeout: any;
|
||||
|
||||
constructor(
|
||||
protected cryptoService: CryptoService,
|
||||
protected apiService: ApiService,
|
||||
protected tokenService: TokenService,
|
||||
protected appIdService: AppIdService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected messagingService: MessagingService,
|
||||
protected logService: LogService,
|
||||
protected keyConnectorService: KeyConnectorService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected stateService: StateService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
protected i18nService: I18nService
|
||||
) {}
|
||||
|
||||
async logIn(
|
||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||
): Promise<AuthResult> {
|
||||
this.clearState();
|
||||
|
||||
let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
||||
|
||||
if (credentials.type === AuthenticationType.Password) {
|
||||
strategy = new PasswordLogInStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this
|
||||
);
|
||||
} else if (credentials.type === AuthenticationType.Sso) {
|
||||
strategy = new SsoLogInStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.keyConnectorService
|
||||
);
|
||||
} else if (credentials.type === AuthenticationType.Api) {
|
||||
strategy = new ApiLogInStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.environmentService,
|
||||
this.keyConnectorService
|
||||
);
|
||||
}
|
||||
|
||||
const result = await strategy.logIn(credentials as any);
|
||||
|
||||
if (result?.requiresTwoFactor) {
|
||||
this.saveState(strategy);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenRequestTwoFactor,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
if (this.logInStrategy == null) {
|
||||
throw new Error(this.i18nService.t("sessionTimeout"));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse);
|
||||
|
||||
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again
|
||||
if (!result.requiresTwoFactor && !result.requiresCaptcha) {
|
||||
this.clearState();
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
// API exceptions are okay, but if there are any unhandled client-side errors then clear state to be safe
|
||||
if (!(e instanceof ErrorResponse)) {
|
||||
this.clearState();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
logOut(callback: () => void) {
|
||||
callback();
|
||||
this.messagingService.send("loggedOut");
|
||||
}
|
||||
|
||||
authingWithApiKey(): boolean {
|
||||
return this.logInStrategy instanceof ApiLogInStrategy;
|
||||
}
|
||||
|
||||
authingWithSso(): boolean {
|
||||
return this.logInStrategy instanceof SsoLogInStrategy;
|
||||
}
|
||||
|
||||
authingWithPassword(): boolean {
|
||||
return this.logInStrategy instanceof PasswordLogInStrategy;
|
||||
}
|
||||
|
||||
async getAuthStatus(userId?: string): Promise<AuthenticationStatus> {
|
||||
const isAuthenticated = await this.stateService.getIsAuthenticated({ userId: userId });
|
||||
if (!isAuthenticated) {
|
||||
return AuthenticationStatus.LoggedOut;
|
||||
}
|
||||
|
||||
// Keys aren't stored for a device that is locked or logged out
|
||||
// Make sure we're logged in before checking this, otherwise we could mix up those states
|
||||
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);
|
||||
}
|
||||
|
||||
const hasKeyInMemory = await this.cryptoService.hasKeyInMemory(userId);
|
||||
if (!hasKeyInMemory) {
|
||||
return AuthenticationStatus.Locked;
|
||||
}
|
||||
|
||||
return AuthenticationStatus.Unlocked;
|
||||
}
|
||||
|
||||
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
|
||||
email = email.trim().toLowerCase();
|
||||
let kdf: KdfType = null;
|
||||
let kdfIterations: number = null;
|
||||
try {
|
||||
const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email));
|
||||
if (preloginResponse != null) {
|
||||
kdf = preloginResponse.kdf;
|
||||
kdfIterations = preloginResponse.kdfIterations;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e == null || e.statusCode !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
|
||||
}
|
||||
|
||||
private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
|
||||
this.logInStrategy = strategy;
|
||||
this.startSessionTimeout();
|
||||
}
|
||||
|
||||
private clearState() {
|
||||
this.logInStrategy = null;
|
||||
this.clearSessionTimeout();
|
||||
}
|
||||
|
||||
private startSessionTimeout() {
|
||||
this.clearSessionTimeout();
|
||||
this.sessionTimeout = setTimeout(() => this.clearState(), sessionTimeoutLength);
|
||||
}
|
||||
|
||||
private clearSessionTimeout() {
|
||||
if (this.sessionTimeout != null) {
|
||||
clearTimeout(this.sessionTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
214
libs/common/src/services/azureFileUpload.service.ts
Normal file
214
libs/common/src/services/azureFileUpload.service.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
34
libs/common/src/services/bitwardenFileUpload.service.ts
Normal file
34
libs/common/src/services/bitwardenFileUpload.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
28
libs/common/src/services/broadcaster.service.ts
Normal file
28
libs/common/src/services/broadcaster.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "../abstractions/broadcaster.service";
|
||||
|
||||
export class BroadcasterService implements BroadcasterServiceAbstraction {
|
||||
subscribers: Map<string, (message: any) => any> = new Map<string, (message: any) => any>();
|
||||
|
||||
send(message: any, id?: string) {
|
||||
if (id != null) {
|
||||
if (this.subscribers.has(id)) {
|
||||
this.subscribers.get(id)(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscribers.forEach((value) => {
|
||||
value(message);
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(id: string, messageCallback: (message: any) => any) {
|
||||
this.subscribers.set(id, messageCallback);
|
||||
}
|
||||
|
||||
unsubscribe(id: string) {
|
||||
if (this.subscribers.has(id)) {
|
||||
this.subscribers.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
1273
libs/common/src/services/cipher.service.ts
Normal file
1273
libs/common/src/services/cipher.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
157
libs/common/src/services/collection.service.ts
Normal file
157
libs/common/src/services/collection.service.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
72
libs/common/src/services/consoleLog.service.ts
Normal file
72
libs/common/src/services/consoleLog.service.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as hrtime from "browser-hrtime";
|
||||
|
||||
import { LogService as LogServiceAbstraction } from "../abstractions/log.service";
|
||||
import { LogLevelType } from "../enums/logLevelType";
|
||||
|
||||
export class ConsoleLogService implements LogServiceAbstraction {
|
||||
protected timersMap: Map<string, [number, number]> = new Map();
|
||||
|
||||
constructor(
|
||||
protected isDev: boolean,
|
||||
protected filter: (level: LogLevelType) => boolean = null
|
||||
) {}
|
||||
|
||||
debug(message: string) {
|
||||
if (!this.isDev) {
|
||||
return;
|
||||
}
|
||||
this.write(LogLevelType.Debug, message);
|
||||
}
|
||||
|
||||
info(message: string) {
|
||||
this.write(LogLevelType.Info, message);
|
||||
}
|
||||
|
||||
warning(message: string) {
|
||||
this.write(LogLevelType.Warning, message);
|
||||
}
|
||||
|
||||
error(message: string) {
|
||||
this.write(LogLevelType.Error, message);
|
||||
}
|
||||
|
||||
write(level: LogLevelType, message: string) {
|
||||
if (this.filter != null && this.filter(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (level) {
|
||||
case LogLevelType.Debug:
|
||||
// eslint-disable-next-line
|
||||
console.log(message);
|
||||
break;
|
||||
case LogLevelType.Info:
|
||||
// eslint-disable-next-line
|
||||
console.log(message);
|
||||
break;
|
||||
case LogLevelType.Warning:
|
||||
// eslint-disable-next-line
|
||||
console.warn(message);
|
||||
break;
|
||||
case LogLevelType.Error:
|
||||
// eslint-disable-next-line
|
||||
console.error(message);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
time(label = "default") {
|
||||
if (!this.timersMap.has(label)) {
|
||||
this.timersMap.set(label, hrtime());
|
||||
}
|
||||
}
|
||||
|
||||
timeEnd(label = "default"): [number, number] {
|
||||
const elapsed = hrtime(this.timersMap.get(label));
|
||||
this.timersMap.delete(label);
|
||||
this.write(LogLevelType.Info, `${label}: ${elapsed[0] * 1000 + elapsed[1] / 10e6}ms`);
|
||||
return elapsed;
|
||||
}
|
||||
}
|
||||
20
libs/common/src/services/container.service.ts
Normal file
20
libs/common/src/services/container.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
|
||||
export class ContainerService {
|
||||
constructor(private cryptoService: CryptoService) {}
|
||||
|
||||
// deprecated, use attachToGlobal instead
|
||||
attachToWindow(win: any) {
|
||||
this.attachToGlobal(win);
|
||||
}
|
||||
|
||||
attachToGlobal(global: any) {
|
||||
if (!global.bitwardenContainerService) {
|
||||
global.bitwardenContainerService = this;
|
||||
}
|
||||
}
|
||||
|
||||
getCryptoService(): CryptoService {
|
||||
return this.cryptoService;
|
||||
}
|
||||
}
|
||||
969
libs/common/src/services/crypto.service.ts
Normal file
969
libs/common/src/services/crypto.service.ts
Normal file
@@ -0,0 +1,969 @@
|
||||
import * as bigInt from "big-integer";
|
||||
|
||||
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EncryptionType } from "../enums/encryptionType";
|
||||
import { HashPurpose } from "../enums/hashPurpose";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { KeySuffixOptions } from "../enums/keySuffixOptions";
|
||||
import { sequentialize } from "../misc/sequentialize";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { EEFLongWordList } from "../misc/wordlist";
|
||||
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
|
||||
import { EncString } from "../models/domain/encString";
|
||||
import { EncryptedObject } from "../models/domain/encryptedObject";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse";
|
||||
import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse";
|
||||
import { ProfileProviderResponse } from "../models/response/profileProviderResponse";
|
||||
|
||||
export class CryptoService implements CryptoServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
protected platformUtilService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
protected stateService: StateService
|
||||
) {}
|
||||
|
||||
async setKey(key: SymmetricCryptoKey, userId?: string): Promise<any> {
|
||||
await this.stateService.setCryptoMasterKey(key, { userId: userId });
|
||||
await this.storeKey(key, userId);
|
||||
}
|
||||
|
||||
async setKeyHash(keyHash: string): Promise<void> {
|
||||
await this.stateService.setKeyHash(keyHash);
|
||||
}
|
||||
|
||||
async setEncKey(encKey: string): Promise<void> {
|
||||
if (encKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedCryptoSymmetricKey(null);
|
||||
await this.stateService.setEncryptedCryptoSymmetricKey(encKey);
|
||||
}
|
||||
|
||||
async setEncPrivateKey(encPrivateKey: string): Promise<void> {
|
||||
if (encPrivateKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedPrivateKey(null);
|
||||
await this.stateService.setEncryptedPrivateKey(encPrivateKey);
|
||||
}
|
||||
|
||||
async setOrgKeys(
|
||||
orgs: ProfileOrganizationResponse[],
|
||||
providerOrgs: ProfileProviderOrganizationResponse[]
|
||||
): Promise<void> {
|
||||
const orgKeys: any = {};
|
||||
orgs.forEach((org) => {
|
||||
orgKeys[org.id] = org.key;
|
||||
});
|
||||
|
||||
for (const providerOrg of providerOrgs) {
|
||||
// Convert provider encrypted keys to user encrypted.
|
||||
const providerKey = await this.getProviderKey(providerOrg.providerId);
|
||||
const decValue = await this.decryptToBytes(new EncString(providerOrg.key), providerKey);
|
||||
orgKeys[providerOrg.id] = (await this.rsaEncrypt(decValue)).encryptedString;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedOrganizationKeys(null);
|
||||
return await this.stateService.setEncryptedOrganizationKeys(orgKeys);
|
||||
}
|
||||
|
||||
async setProviderKeys(providers: ProfileProviderResponse[]): Promise<void> {
|
||||
const providerKeys: any = {};
|
||||
providers.forEach((provider) => {
|
||||
providerKeys[provider.id] = provider.key;
|
||||
});
|
||||
|
||||
await this.stateService.setDecryptedProviderKeys(null);
|
||||
return await this.stateService.setEncryptedProviderKeys(providerKeys);
|
||||
}
|
||||
|
||||
async getKey(keySuffix?: KeySuffixOptions, userId?: string): Promise<SymmetricCryptoKey> {
|
||||
const inMemoryKey = await this.stateService.getCryptoMasterKey({ userId: userId });
|
||||
|
||||
if (inMemoryKey != null) {
|
||||
return inMemoryKey;
|
||||
}
|
||||
|
||||
keySuffix ||= KeySuffixOptions.Auto;
|
||||
const symmetricKey = await this.getKeyFromStorage(keySuffix, userId);
|
||||
|
||||
if (symmetricKey != null) {
|
||||
// TODO: Refactor here so get key doesn't also set key
|
||||
this.setKey(symmetricKey, userId);
|
||||
}
|
||||
|
||||
return symmetricKey;
|
||||
}
|
||||
|
||||
async getKeyFromStorage(
|
||||
keySuffix: KeySuffixOptions,
|
||||
userId?: string
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
const key = await this.retrieveKeyFromStorage(keySuffix, userId);
|
||||
if (key != null) {
|
||||
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer);
|
||||
|
||||
if (!(await this.validateKey(symmetricKey))) {
|
||||
this.logService.warning("Wrong key, throwing away stored key");
|
||||
await this.clearSecretKeyStore(userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return symmetricKey;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getKeyHash(): Promise<string> {
|
||||
return await this.stateService.getKeyHash();
|
||||
}
|
||||
|
||||
async compareAndUpdateKeyHash(masterPassword: string, key: SymmetricCryptoKey): Promise<boolean> {
|
||||
const storedKeyHash = await this.getKeyHash();
|
||||
if (masterPassword != null && storedKeyHash != null) {
|
||||
const localKeyHash = await this.hashPassword(
|
||||
masterPassword,
|
||||
key,
|
||||
HashPurpose.LocalAuthorization
|
||||
);
|
||||
if (localKeyHash != null && storedKeyHash === localKeyHash) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: remove serverKeyHash check in 1-2 releases after everyone's keyHash has been updated
|
||||
const serverKeyHash = await this.hashPassword(
|
||||
masterPassword,
|
||||
key,
|
||||
HashPurpose.ServerAuthorization
|
||||
);
|
||||
if (serverKeyHash != null && storedKeyHash === serverKeyHash) {
|
||||
await this.setKeyHash(localKeyHash);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@sequentialize(() => "getEncKey")
|
||||
getEncKey(key: SymmetricCryptoKey = null): Promise<SymmetricCryptoKey> {
|
||||
return this.getEncKeyHelper(key);
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<ArrayBuffer> {
|
||||
const inMemoryPublicKey = await this.stateService.getPublicKey();
|
||||
if (inMemoryPublicKey != null) {
|
||||
return inMemoryPublicKey;
|
||||
}
|
||||
|
||||
const privateKey = await this.getPrivateKey();
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
await this.stateService.setPublicKey(publicKey);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
async getPrivateKey(): Promise<ArrayBuffer> {
|
||||
const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey();
|
||||
if (decryptedPrivateKey != null) {
|
||||
return decryptedPrivateKey;
|
||||
}
|
||||
|
||||
const encPrivateKey = await this.stateService.getEncryptedPrivateKey();
|
||||
if (encPrivateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), null);
|
||||
await this.stateService.setDecryptedPrivateKey(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise<string[]> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
}
|
||||
if (publicKey === null) {
|
||||
throw new Error("No public key available.");
|
||||
}
|
||||
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
|
||||
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
|
||||
keyFingerprint,
|
||||
userId,
|
||||
32,
|
||||
"sha256"
|
||||
);
|
||||
return this.hashPhrase(userFingerprint);
|
||||
}
|
||||
|
||||
@sequentialize(() => "getOrgKeys")
|
||||
async getOrgKeys(): Promise<Map<string, SymmetricCryptoKey>> {
|
||||
const orgKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
|
||||
const decryptedOrganizationKeys = await this.stateService.getDecryptedOrganizationKeys();
|
||||
if (decryptedOrganizationKeys != null && decryptedOrganizationKeys.size > 0) {
|
||||
return decryptedOrganizationKeys;
|
||||
}
|
||||
|
||||
const encOrgKeys = await this.stateService.getEncryptedOrganizationKeys();
|
||||
if (encOrgKeys == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let setKey = false;
|
||||
|
||||
for (const orgId in encOrgKeys) {
|
||||
// eslint-disable-next-line
|
||||
if (!encOrgKeys.hasOwnProperty(orgId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decValue = await this.rsaDecrypt(encOrgKeys[orgId]);
|
||||
orgKeys.set(orgId, new SymmetricCryptoKey(decValue));
|
||||
setKey = true;
|
||||
}
|
||||
|
||||
if (setKey) {
|
||||
await this.stateService.setDecryptedOrganizationKeys(orgKeys);
|
||||
}
|
||||
|
||||
return orgKeys;
|
||||
}
|
||||
|
||||
async getOrgKey(orgId: string): Promise<SymmetricCryptoKey> {
|
||||
if (orgId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgKeys = await this.getOrgKeys();
|
||||
if (orgKeys == null || !orgKeys.has(orgId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return orgKeys.get(orgId);
|
||||
}
|
||||
|
||||
@sequentialize(() => "getProviderKeys")
|
||||
async getProviderKeys(): Promise<Map<string, SymmetricCryptoKey>> {
|
||||
const providerKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
|
||||
const decryptedProviderKeys = await this.stateService.getDecryptedProviderKeys();
|
||||
if (decryptedProviderKeys != null && decryptedProviderKeys.size > 0) {
|
||||
return decryptedProviderKeys;
|
||||
}
|
||||
|
||||
const encProviderKeys = await this.stateService.getEncryptedProviderKeys();
|
||||
if (encProviderKeys == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let setKey = false;
|
||||
|
||||
for (const orgId in encProviderKeys) {
|
||||
// eslint-disable-next-line
|
||||
if (!encProviderKeys.hasOwnProperty(orgId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decValue = await this.rsaDecrypt(encProviderKeys[orgId]);
|
||||
providerKeys.set(orgId, new SymmetricCryptoKey(decValue));
|
||||
setKey = true;
|
||||
}
|
||||
|
||||
if (setKey) {
|
||||
await this.stateService.setDecryptedProviderKeys(providerKeys);
|
||||
}
|
||||
|
||||
return providerKeys;
|
||||
}
|
||||
|
||||
async getProviderKey(providerId: string): Promise<SymmetricCryptoKey> {
|
||||
if (providerId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerKeys = await this.getProviderKeys();
|
||||
if (providerKeys == null || !providerKeys.has(providerId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return providerKeys.get(providerId);
|
||||
}
|
||||
|
||||
async hasKey(): Promise<boolean> {
|
||||
return (
|
||||
(await this.hasKeyInMemory()) ||
|
||||
(await this.hasKeyStored(KeySuffixOptions.Auto)) ||
|
||||
(await this.hasKeyStored(KeySuffixOptions.Biometric))
|
||||
);
|
||||
}
|
||||
|
||||
async hasKeyInMemory(userId?: string): Promise<boolean> {
|
||||
return (await this.stateService.getCryptoMasterKey({ userId: userId })) != null;
|
||||
}
|
||||
|
||||
async hasKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
|
||||
switch (keySuffix) {
|
||||
case KeySuffixOptions.Auto:
|
||||
return (await this.stateService.getCryptoMasterKeyAuto({ userId: userId })) != null;
|
||||
case KeySuffixOptions.Biometric:
|
||||
return (await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId })) === true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async hasEncKey(): Promise<boolean> {
|
||||
return (await this.stateService.getEncryptedCryptoSymmetricKey()) != null;
|
||||
}
|
||||
|
||||
async clearKey(clearSecretStorage = true, userId?: string): Promise<any> {
|
||||
await this.stateService.setCryptoMasterKey(null, { userId: userId });
|
||||
await this.stateService.setLegacyEtmKey(null, { userId: userId });
|
||||
if (clearSecretStorage) {
|
||||
await this.clearSecretKeyStore(userId);
|
||||
}
|
||||
}
|
||||
|
||||
async clearStoredKey(keySuffix: KeySuffixOptions) {
|
||||
keySuffix === KeySuffixOptions.Auto
|
||||
? await this.stateService.setCryptoMasterKeyAuto(null)
|
||||
: await this.stateService.setCryptoMasterKeyBiometric(null);
|
||||
}
|
||||
|
||||
async clearKeyHash(userId?: string): Promise<any> {
|
||||
return await this.stateService.setKeyHash(null, { userId: userId });
|
||||
}
|
||||
|
||||
async clearEncKey(memoryOnly?: boolean, userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedCryptoSymmetricKey(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedCryptoSymmetricKey(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearKeyPair(memoryOnly?: boolean, userId?: string): Promise<any> {
|
||||
const keysToClear: Promise<void>[] = [
|
||||
this.stateService.setDecryptedPrivateKey(null, { userId: userId }),
|
||||
this.stateService.setPublicKey(null, { userId: userId }),
|
||||
];
|
||||
if (!memoryOnly) {
|
||||
keysToClear.push(this.stateService.setEncryptedPrivateKey(null, { userId: userId }));
|
||||
}
|
||||
return Promise.all(keysToClear);
|
||||
}
|
||||
|
||||
async clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedOrganizationKeys(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedOrganizationKeys(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise<void> {
|
||||
await this.stateService.setDecryptedProviderKeys(null, { userId: userId });
|
||||
if (!memoryOnly) {
|
||||
await this.stateService.setEncryptedProviderKeys(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
async clearPinProtectedKey(userId?: string): Promise<any> {
|
||||
return await this.stateService.setEncryptedPinProtected(null, { userId: userId });
|
||||
}
|
||||
|
||||
async clearKeys(userId?: string): Promise<any> {
|
||||
await this.clearKey(true, userId);
|
||||
await this.clearKeyHash(userId);
|
||||
await this.clearOrgKeys(false, userId);
|
||||
await this.clearProviderKeys(false, userId);
|
||||
await this.clearEncKey(false, userId);
|
||||
await this.clearKeyPair(false, userId);
|
||||
await this.clearPinProtectedKey(userId);
|
||||
}
|
||||
|
||||
async toggleKey(): Promise<any> {
|
||||
const key = await this.getKey();
|
||||
|
||||
await this.setKey(key);
|
||||
}
|
||||
|
||||
async makeKey(
|
||||
password: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
let key: ArrayBuffer = null;
|
||||
if (kdf == null || kdf === KdfType.PBKDF2_SHA256) {
|
||||
if (kdfIterations == null) {
|
||||
kdfIterations = 5000;
|
||||
} else if (kdfIterations < 5000) {
|
||||
throw new Error("PBKDF2 iteration minimum is 5000.");
|
||||
}
|
||||
key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfIterations);
|
||||
} else {
|
||||
throw new Error("Unknown Kdf.");
|
||||
}
|
||||
return new SymmetricCryptoKey(key);
|
||||
}
|
||||
|
||||
async makeKeyFromPin(
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number,
|
||||
protectedKeyCs: EncString = null
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
if (protectedKeyCs == null) {
|
||||
const pinProtectedKey = await this.stateService.getEncryptedPinProtected();
|
||||
if (pinProtectedKey == null) {
|
||||
throw new Error("No PIN protected key found.");
|
||||
}
|
||||
protectedKeyCs = new EncString(pinProtectedKey);
|
||||
}
|
||||
const pinKey = await this.makePinKey(pin, salt, kdf, kdfIterations);
|
||||
const decKey = await this.decryptToBytes(protectedKeyCs, pinKey);
|
||||
return new SymmetricCryptoKey(decKey);
|
||||
}
|
||||
|
||||
async makeShareKey(): Promise<[EncString, SymmetricCryptoKey]> {
|
||||
const shareKey = await this.cryptoFunctionService.randomBytes(64);
|
||||
const publicKey = await this.getPublicKey();
|
||||
const encShareKey = await this.rsaEncrypt(shareKey, publicKey);
|
||||
return [encShareKey, new SymmetricCryptoKey(shareKey)];
|
||||
}
|
||||
|
||||
async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> {
|
||||
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
|
||||
const privateEnc = await this.encrypt(keyPair[1], key);
|
||||
return [publicB64, privateEnc];
|
||||
}
|
||||
|
||||
async makePinKey(
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfIterations: number
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
const pinKey = await this.makeKey(pin, salt, kdf, kdfIterations);
|
||||
return await this.stretchKey(pinKey);
|
||||
}
|
||||
|
||||
async makeSendKey(keyMaterial: ArrayBuffer): Promise<SymmetricCryptoKey> {
|
||||
const sendKey = await this.cryptoFunctionService.hkdf(
|
||||
keyMaterial,
|
||||
"bitwarden-send",
|
||||
"send",
|
||||
64,
|
||||
"sha256"
|
||||
);
|
||||
return new SymmetricCryptoKey(sendKey);
|
||||
}
|
||||
|
||||
async hashPassword(
|
||||
password: string,
|
||||
key: SymmetricCryptoKey,
|
||||
hashPurpose?: HashPurpose
|
||||
): Promise<string> {
|
||||
if (key == null) {
|
||||
key = await this.getKey();
|
||||
}
|
||||
if (password == null || key == null) {
|
||||
throw new Error("Invalid parameters.");
|
||||
}
|
||||
|
||||
const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1;
|
||||
const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, "sha256", iterations);
|
||||
return Utils.fromBufferToB64(hash);
|
||||
}
|
||||
|
||||
async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, EncString]> {
|
||||
const theKey = await this.getKeyForEncryption(key);
|
||||
const encKey = await this.cryptoFunctionService.randomBytes(64);
|
||||
return this.buildEncKey(theKey, encKey);
|
||||
}
|
||||
|
||||
async remakeEncKey(
|
||||
key: SymmetricCryptoKey,
|
||||
encKey?: SymmetricCryptoKey
|
||||
): Promise<[SymmetricCryptoKey, EncString]> {
|
||||
if (encKey == null) {
|
||||
encKey = await this.getEncKey();
|
||||
}
|
||||
return this.buildEncKey(key, encKey.key);
|
||||
}
|
||||
|
||||
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (plainValue == null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
let plainBuf: ArrayBuffer;
|
||||
if (typeof plainValue === "string") {
|
||||
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
|
||||
} else {
|
||||
plainBuf = plainValue;
|
||||
}
|
||||
|
||||
const encObj = await this.aesEncrypt(plainBuf, key);
|
||||
const iv = Utils.fromBufferToB64(encObj.iv);
|
||||
const data = Utils.fromBufferToB64(encObj.data);
|
||||
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
|
||||
return new EncString(encObj.key.encType, data, iv, mac);
|
||||
}
|
||||
|
||||
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
const encValue = await this.aesEncrypt(plainValue, key);
|
||||
let macLen = 0;
|
||||
if (encValue.mac != null) {
|
||||
macLen = encValue.mac.byteLength;
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
|
||||
encBytes.set([encValue.key.encType]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
if (encValue.mac != null) {
|
||||
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
|
||||
}
|
||||
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes.buffer);
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise<EncString> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
}
|
||||
if (publicKey == null) {
|
||||
throw new Error("Public key unavailable.");
|
||||
}
|
||||
|
||||
const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes));
|
||||
}
|
||||
|
||||
async rsaDecrypt(encValue: string, privateKeyValue?: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const headerPieces = encValue.split(".");
|
||||
let encType: EncryptionType = null;
|
||||
let encPieces: string[];
|
||||
|
||||
if (headerPieces.length === 1) {
|
||||
encType = EncryptionType.Rsa2048_OaepSha256_B64;
|
||||
encPieces = [headerPieces[0]];
|
||||
} else if (headerPieces.length === 2) {
|
||||
try {
|
||||
encType = parseInt(headerPieces[0], null);
|
||||
encPieces = headerPieces[1].split("|");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: // HmacSha256 types are deprecated
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
break;
|
||||
default:
|
||||
throw new Error("encType unavailable.");
|
||||
}
|
||||
|
||||
if (encPieces == null || encPieces.length <= 0) {
|
||||
throw new Error("encPieces unavailable.");
|
||||
}
|
||||
|
||||
const data = Utils.fromB64ToArray(encPieces[0]).buffer;
|
||||
const privateKey = privateKeyValue ?? (await this.getPrivateKey());
|
||||
if (privateKey == null) {
|
||||
throw new Error("No private key.");
|
||||
}
|
||||
|
||||
let alg: "sha1" | "sha256" = "sha1";
|
||||
switch (encType) {
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
alg = "sha256";
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
break;
|
||||
default:
|
||||
throw new Error("encType unavailable.");
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg);
|
||||
}
|
||||
|
||||
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
const iv = Utils.fromB64ToArray(encString.iv).buffer;
|
||||
const data = Utils.fromB64ToArray(encString.data).buffer;
|
||||
const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer : null;
|
||||
const decipher = await this.aesDecryptToBytes(encString.encryptionType, data, iv, mac, key);
|
||||
if (decipher == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decipher;
|
||||
}
|
||||
|
||||
async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string> {
|
||||
return await this.aesDecryptToUtf8(
|
||||
encString.encryptionType,
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
if (encBuf == null) {
|
||||
throw new Error("no encBuf.");
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(encBuf);
|
||||
const encType = encBytes[0];
|
||||
let ctBytes: Uint8Array = null;
|
||||
let ivBytes: Uint8Array = null;
|
||||
let macBytes: Uint8Array = null;
|
||||
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if (encBytes.length <= 49) {
|
||||
// 1 + 16 + 32 + ctLength
|
||||
return null;
|
||||
}
|
||||
|
||||
ivBytes = encBytes.slice(1, 17);
|
||||
macBytes = encBytes.slice(17, 49);
|
||||
ctBytes = encBytes.slice(49);
|
||||
break;
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if (encBytes.length <= 17) {
|
||||
// 1 + 16 + ctLength
|
||||
return null;
|
||||
}
|
||||
|
||||
ivBytes = encBytes.slice(1, 17);
|
||||
ctBytes = encBytes.slice(17);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.aesDecryptToBytes(
|
||||
encType,
|
||||
ctBytes.buffer,
|
||||
ivBytes.buffer,
|
||||
macBytes != null ? macBytes.buffer : null,
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
// EFForg/OpenWireless
|
||||
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
|
||||
async randomNumber(min: number, max: number): Promise<number> {
|
||||
let rval = 0;
|
||||
const range = max - min + 1;
|
||||
const bitsNeeded = Math.ceil(Math.log2(range));
|
||||
if (bitsNeeded > 53) {
|
||||
throw new Error("We cannot generate numbers larger than 53 bits.");
|
||||
}
|
||||
|
||||
const bytesNeeded = Math.ceil(bitsNeeded / 8);
|
||||
const mask = Math.pow(2, bitsNeeded) - 1;
|
||||
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
|
||||
|
||||
// Fill a byte array with N random numbers
|
||||
const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded));
|
||||
|
||||
let p = (bytesNeeded - 1) * 8;
|
||||
for (let i = 0; i < bytesNeeded; i++) {
|
||||
rval += byteArray[i] * Math.pow(2, p);
|
||||
p -= 8;
|
||||
}
|
||||
|
||||
// Use & to apply the mask and reduce the number of recursive lookups
|
||||
rval = rval & mask;
|
||||
|
||||
if (rval >= range) {
|
||||
// Integer out of acceptable range
|
||||
return this.randomNumber(min, max);
|
||||
}
|
||||
|
||||
// Return an integer that falls within the range
|
||||
return min + rval;
|
||||
}
|
||||
|
||||
async validateKey(key: SymmetricCryptoKey) {
|
||||
try {
|
||||
const encPrivateKey = await this.stateService.getEncryptedPrivateKey();
|
||||
const encKey = await this.getEncKeyHelper(key);
|
||||
if (encPrivateKey == null || encKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey);
|
||||
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||
} else if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string) {
|
||||
let shouldStoreKey = false;
|
||||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId });
|
||||
shouldStoreKey = vaultTimeout == null;
|
||||
} else if (keySuffix === KeySuffixOptions.Biometric) {
|
||||
const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId });
|
||||
shouldStoreKey = biometricUnlock && this.platformUtilService.supportsSecureStorage();
|
||||
}
|
||||
return shouldStoreKey;
|
||||
}
|
||||
|
||||
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string) {
|
||||
return keySuffix === KeySuffixOptions.Auto
|
||||
? await this.stateService.getCryptoMasterKeyAuto({ userId: userId })
|
||||
: await this.stateService.getCryptoMasterKeyBiometric({ userId: userId });
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = await this.getKeyForEncryption(key);
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
|
||||
|
||||
if (obj.key.macKey != null) {
|
||||
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
|
||||
macData.set(new Uint8Array(obj.iv), 0);
|
||||
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
|
||||
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private async aesDecryptToUtf8(
|
||||
encType: EncryptionType,
|
||||
data: string,
|
||||
iv: string,
|
||||
mac: string,
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<string> {
|
||||
const keyForEnc = await this.getKeyForEncryption(key);
|
||||
const theKey = await this.resolveLegacyKey(encType, keyForEnc);
|
||||
|
||||
if (theKey.macKey != null && mac == null) {
|
||||
this.logService.error("mac required.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.encType !== encType) {
|
||||
this.logService.error("encType unavailable.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(data, iv, mac, theKey);
|
||||
if (fastParams.macKey != null && fastParams.mac != null) {
|
||||
const computedMac = await this.cryptoFunctionService.hmacFast(
|
||||
fastParams.macData,
|
||||
fastParams.macKey,
|
||||
"sha256"
|
||||
);
|
||||
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
||||
if (!macsEqual) {
|
||||
this.logService.error("mac failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.aesDecryptFast(fastParams);
|
||||
}
|
||||
|
||||
private async aesDecryptToBytes(
|
||||
encType: EncryptionType,
|
||||
data: ArrayBuffer,
|
||||
iv: ArrayBuffer,
|
||||
mac: ArrayBuffer,
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<ArrayBuffer> {
|
||||
const keyForEnc = await this.getKeyForEncryption(key);
|
||||
const theKey = await this.resolveLegacyKey(encType, keyForEnc);
|
||||
|
||||
if (theKey.macKey != null && mac == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.encType !== encType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.macKey != null && mac != null) {
|
||||
const macData = new Uint8Array(iv.byteLength + data.byteLength);
|
||||
macData.set(new Uint8Array(iv), 0);
|
||||
macData.set(new Uint8Array(data), iv.byteLength);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(
|
||||
macData.buffer,
|
||||
theKey.macKey,
|
||||
"sha256"
|
||||
);
|
||||
if (computedMac === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac);
|
||||
if (!macsMatch) {
|
||||
this.logService.error("mac failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey);
|
||||
}
|
||||
|
||||
private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
const encKey = await this.getEncKey();
|
||||
if (encKey != null) {
|
||||
return encKey;
|
||||
}
|
||||
|
||||
return await this.getKey();
|
||||
}
|
||||
|
||||
private async resolveLegacyKey(
|
||||
encType: EncryptionType,
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
if (
|
||||
encType === EncryptionType.AesCbc128_HmacSha256_B64 &&
|
||||
key.encType === EncryptionType.AesCbc256_B64
|
||||
) {
|
||||
// Old encrypt-then-mac scheme, make a new key
|
||||
let legacyKey = await this.stateService.getLegacyEtmKey();
|
||||
if (legacyKey == null) {
|
||||
legacyKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
await this.stateService.setLegacyEtmKey(legacyKey);
|
||||
}
|
||||
return legacyKey;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
private async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||
const newKey = new Uint8Array(64);
|
||||
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
|
||||
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
|
||||
newKey.set(new Uint8Array(encKey));
|
||||
newKey.set(new Uint8Array(macKey), 32);
|
||||
return new SymmetricCryptoKey(newKey.buffer);
|
||||
}
|
||||
|
||||
private async hashPhrase(hash: ArrayBuffer, minimumEntropy = 64) {
|
||||
const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2);
|
||||
let numWords = Math.ceil(minimumEntropy / entropyPerWord);
|
||||
|
||||
const hashArr = Array.from(new Uint8Array(hash));
|
||||
const entropyAvailable = hashArr.length * 4;
|
||||
if (numWords * entropyPerWord > entropyAvailable) {
|
||||
throw new Error("Output entropy of hash function is too small");
|
||||
}
|
||||
|
||||
const phrase: string[] = [];
|
||||
let hashNumber = bigInt.fromArray(hashArr, 256);
|
||||
while (numWords--) {
|
||||
const remainder = hashNumber.mod(EEFLongWordList.length);
|
||||
hashNumber = hashNumber.divide(EEFLongWordList.length);
|
||||
phrase.push(EEFLongWordList[remainder as any]);
|
||||
}
|
||||
return phrase;
|
||||
}
|
||||
|
||||
private async buildEncKey(
|
||||
key: SymmetricCryptoKey,
|
||||
encKey: ArrayBuffer
|
||||
): Promise<[SymmetricCryptoKey, EncString]> {
|
||||
let encKeyEnc: EncString = null;
|
||||
if (key.key.byteLength === 32) {
|
||||
const newKey = await this.stretchKey(key);
|
||||
encKeyEnc = await this.encrypt(encKey, newKey);
|
||||
} else if (key.key.byteLength === 64) {
|
||||
encKeyEnc = await this.encrypt(encKey, key);
|
||||
} else {
|
||||
throw new Error("Invalid key size.");
|
||||
}
|
||||
return [new SymmetricCryptoKey(encKey), encKeyEnc];
|
||||
}
|
||||
|
||||
private async clearSecretKeyStore(userId?: string): Promise<void> {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||
}
|
||||
|
||||
private async getEncKeyHelper(key: SymmetricCryptoKey = null): Promise<SymmetricCryptoKey> {
|
||||
const inMemoryKey = await this.stateService.getDecryptedCryptoSymmetricKey();
|
||||
if (inMemoryKey != null) {
|
||||
return inMemoryKey;
|
||||
}
|
||||
|
||||
const encKey = await this.stateService.getEncryptedCryptoSymmetricKey();
|
||||
if (encKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key == null) {
|
||||
key = await this.getKey();
|
||||
}
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let decEncKey: ArrayBuffer;
|
||||
const encKeyCipher = new EncString(encKey);
|
||||
if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
decEncKey = await this.decryptToBytes(encKeyCipher, key);
|
||||
} else if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const newKey = await this.stretchKey(key);
|
||||
decEncKey = await this.decryptToBytes(encKeyCipher, newKey);
|
||||
} else {
|
||||
throw new Error("Unsupported encKey type.");
|
||||
}
|
||||
if (decEncKey == null) {
|
||||
return null;
|
||||
}
|
||||
const symmetricCryptoKey = new SymmetricCryptoKey(decEncKey);
|
||||
await this.stateService.setDecryptedCryptoSymmetricKey(symmetricCryptoKey);
|
||||
return symmetricCryptoKey;
|
||||
}
|
||||
}
|
||||
188
libs/common/src/services/environment.service.ts
Normal file
188
libs/common/src/services/environment.service.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
Urls,
|
||||
} from "../abstractions/environment.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EnvironmentUrls } from "../models/domain/environmentUrls";
|
||||
|
||||
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
private readonly urlsSubject = new Subject<Urls>();
|
||||
urls: Observable<Urls> = this.urlsSubject;
|
||||
|
||||
private baseUrl: string;
|
||||
private webVaultUrl: string;
|
||||
private apiUrl: string;
|
||||
private identityUrl: string;
|
||||
private iconsUrl: string;
|
||||
private notificationsUrl: string;
|
||||
private eventsUrl: string;
|
||||
private keyConnectorUrl: string;
|
||||
|
||||
constructor(private stateService: StateService) {
|
||||
this.stateService.activeAccount.subscribe(async () => {
|
||||
await this.setUrlsFromStorage();
|
||||
});
|
||||
}
|
||||
|
||||
hasBaseUrl() {
|
||||
return this.baseUrl != null;
|
||||
}
|
||||
|
||||
getNotificationsUrl() {
|
||||
if (this.notificationsUrl != null) {
|
||||
return this.notificationsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl != null) {
|
||||
return this.baseUrl + "/notifications";
|
||||
}
|
||||
|
||||
return "https://notifications.bitwarden.com";
|
||||
}
|
||||
|
||||
getWebVaultUrl() {
|
||||
if (this.webVaultUrl != null) {
|
||||
return this.webVaultUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl;
|
||||
}
|
||||
return "https://vault.bitwarden.com";
|
||||
}
|
||||
|
||||
getSendUrl() {
|
||||
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
|
||||
? "https://send.bitwarden.com/#"
|
||||
: this.getWebVaultUrl() + "/#/send/";
|
||||
}
|
||||
|
||||
getIconsUrl() {
|
||||
if (this.iconsUrl != null) {
|
||||
return this.iconsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/icons";
|
||||
}
|
||||
|
||||
return "https://icons.bitwarden.net";
|
||||
}
|
||||
|
||||
getApiUrl() {
|
||||
if (this.apiUrl != null) {
|
||||
return this.apiUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/api";
|
||||
}
|
||||
|
||||
return "https://api.bitwarden.com";
|
||||
}
|
||||
|
||||
getIdentityUrl() {
|
||||
if (this.identityUrl != null) {
|
||||
return this.identityUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/identity";
|
||||
}
|
||||
|
||||
return "https://identity.bitwarden.com";
|
||||
}
|
||||
|
||||
getEventsUrl() {
|
||||
if (this.eventsUrl != null) {
|
||||
return this.eventsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/events";
|
||||
}
|
||||
|
||||
return "https://events.bitwarden.com";
|
||||
}
|
||||
|
||||
getKeyConnectorUrl() {
|
||||
return this.keyConnectorUrl;
|
||||
}
|
||||
|
||||
async setUrlsFromStorage(): Promise<void> {
|
||||
const urls: any = await this.stateService.getEnvironmentUrls();
|
||||
const envUrls = new EnvironmentUrls();
|
||||
|
||||
this.baseUrl = envUrls.base = urls.base;
|
||||
this.webVaultUrl = urls.webVault;
|
||||
this.apiUrl = envUrls.api = urls.api;
|
||||
this.identityUrl = envUrls.identity = urls.identity;
|
||||
this.iconsUrl = urls.icons;
|
||||
this.notificationsUrl = urls.notifications;
|
||||
this.eventsUrl = envUrls.events = urls.events;
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
}
|
||||
|
||||
async setUrls(urls: Urls): Promise<Urls> {
|
||||
urls.base = this.formatUrl(urls.base);
|
||||
urls.webVault = this.formatUrl(urls.webVault);
|
||||
urls.api = this.formatUrl(urls.api);
|
||||
urls.identity = this.formatUrl(urls.identity);
|
||||
urls.icons = this.formatUrl(urls.icons);
|
||||
urls.notifications = this.formatUrl(urls.notifications);
|
||||
urls.events = this.formatUrl(urls.events);
|
||||
urls.keyConnector = this.formatUrl(urls.keyConnector);
|
||||
|
||||
await this.stateService.setEnvironmentUrls({
|
||||
base: urls.base,
|
||||
api: urls.api,
|
||||
identity: urls.identity,
|
||||
webVault: urls.webVault,
|
||||
icons: urls.icons,
|
||||
notifications: urls.notifications,
|
||||
events: urls.events,
|
||||
keyConnector: urls.keyConnector,
|
||||
});
|
||||
|
||||
this.baseUrl = urls.base;
|
||||
this.webVaultUrl = urls.webVault;
|
||||
this.apiUrl = urls.api;
|
||||
this.identityUrl = urls.identity;
|
||||
this.iconsUrl = urls.icons;
|
||||
this.notificationsUrl = urls.notifications;
|
||||
this.eventsUrl = urls.events;
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
|
||||
this.urlsSubject.next(urls);
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
getUrls() {
|
||||
return {
|
||||
base: this.baseUrl,
|
||||
webVault: this.webVaultUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
icons: this.iconsUrl,
|
||||
notifications: this.notificationsUrl,
|
||||
events: this.eventsUrl,
|
||||
keyConnector: this.keyConnectorUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private formatUrl(url: string): string {
|
||||
if (url == null || url === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
url = url.replace(/\/+$/g, "");
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
return url.trim();
|
||||
}
|
||||
}
|
||||
99
libs/common/src/services/event.service.ts
Normal file
99
libs/common/src/services/event.service.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
444
libs/common/src/services/export.service.ts
Normal file
444
libs/common/src/services/export.service.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import * as papa from "papaparse";
|
||||
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { CipherService } from "../abstractions/cipher.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import {
|
||||
ExportFormat,
|
||||
ExportService as ExportServiceAbstraction,
|
||||
} from "../abstractions/export.service";
|
||||
import { FolderService } from "../abstractions/folder.service";
|
||||
import { CipherType } from "../enums/cipherType";
|
||||
import { DEFAULT_KDF_ITERATIONS, KdfType } from "../enums/kdfType";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { CipherData } from "../models/data/cipherData";
|
||||
import { CollectionData } from "../models/data/collectionData";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
import { Collection } from "../models/domain/collection";
|
||||
import { Folder } from "../models/domain/folder";
|
||||
import { CipherWithIdExport as CipherExport } from "../models/export/cipherWithIdsExport";
|
||||
import { CollectionWithIdExport as CollectionExport } from "../models/export/collectionWithIdExport";
|
||||
import { EventExport } from "../models/export/eventExport";
|
||||
import { FolderWithIdExport as FolderExport } from "../models/export/folderWithIdExport";
|
||||
import { CollectionDetailsResponse } from "../models/response/collectionResponse";
|
||||
import { CipherView } from "../models/view/cipherView";
|
||||
import { CollectionView } from "../models/view/collectionView";
|
||||
import { EventView } from "../models/view/eventView";
|
||||
import { FolderView } from "../models/view/folderView";
|
||||
|
||||
export class ExportService implements ExportServiceAbstraction {
|
||||
constructor(
|
||||
private folderService: FolderService,
|
||||
private cipherService: CipherService,
|
||||
private apiService: ApiService,
|
||||
private cryptoService: CryptoService,
|
||||
private cryptoFunctionService: CryptoFunctionService
|
||||
) {}
|
||||
|
||||
async getExport(format: ExportFormat = "csv", organizationId?: string): Promise<string> {
|
||||
if (organizationId) {
|
||||
return await this.getOrganizationExport(organizationId, format);
|
||||
}
|
||||
|
||||
if (format === "encrypted_json") {
|
||||
return this.getEncryptedExport();
|
||||
} else {
|
||||
return this.getDecryptedExport(format);
|
||||
}
|
||||
}
|
||||
|
||||
async getPasswordProtectedExport(password: string, organizationId?: string): Promise<string> {
|
||||
const clearText = organizationId
|
||||
? await this.getOrganizationExport(organizationId, "json")
|
||||
: await this.getExport("json");
|
||||
|
||||
const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16));
|
||||
const kdfIterations = DEFAULT_KDF_ITERATIONS;
|
||||
const key = await this.cryptoService.makePinKey(
|
||||
password,
|
||||
salt,
|
||||
KdfType.PBKDF2_SHA256,
|
||||
kdfIterations
|
||||
);
|
||||
|
||||
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key);
|
||||
const encText = await this.cryptoService.encrypt(clearText, key);
|
||||
|
||||
const jsonDoc: any = {
|
||||
encrypted: true,
|
||||
passwordProtected: true,
|
||||
salt: salt,
|
||||
kdfIterations: kdfIterations,
|
||||
kdfType: KdfType.PBKDF2_SHA256,
|
||||
encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString,
|
||||
data: encText.encryptedString,
|
||||
};
|
||||
|
||||
return JSON.stringify(jsonDoc, null, " ");
|
||||
}
|
||||
|
||||
async getOrganizationExport(
|
||||
organizationId: string,
|
||||
format: ExportFormat = "csv"
|
||||
): Promise<string> {
|
||||
if (format === "encrypted_json") {
|
||||
return this.getOrganizationEncryptedExport(organizationId);
|
||||
} else {
|
||||
return this.getOrganizationDecryptedExport(organizationId, format);
|
||||
}
|
||||
}
|
||||
|
||||
async getEventExport(events: EventView[]): Promise<string> {
|
||||
return papa.unparse(events.map((e) => new EventExport(e)));
|
||||
}
|
||||
|
||||
getFileName(prefix: string = null, extension = "csv"): string {
|
||||
const now = new Date();
|
||||
const dateString =
|
||||
now.getFullYear() +
|
||||
"" +
|
||||
this.padNumber(now.getMonth() + 1, 2) +
|
||||
"" +
|
||||
this.padNumber(now.getDate(), 2) +
|
||||
this.padNumber(now.getHours(), 2) +
|
||||
"" +
|
||||
this.padNumber(now.getMinutes(), 2) +
|
||||
this.padNumber(now.getSeconds(), 2);
|
||||
|
||||
return "bitwarden" + (prefix ? "_" + prefix : "") + "_export_" + dateString + "." + extension;
|
||||
}
|
||||
|
||||
private async getDecryptedExport(format: "json" | "csv"): Promise<string> {
|
||||
let decFolders: FolderView[] = [];
|
||||
let decCiphers: CipherView[] = [];
|
||||
const promises = [];
|
||||
|
||||
promises.push(
|
||||
this.folderService.getAllDecrypted().then((folders) => {
|
||||
decFolders = folders;
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.cipherService.getAllDecrypted().then((ciphers) => {
|
||||
decCiphers = ciphers.filter((f) => f.deletedDate == null);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (format === "csv") {
|
||||
const foldersMap = new Map<string, FolderView>();
|
||||
decFolders.forEach((f) => {
|
||||
if (f.id != null) {
|
||||
foldersMap.set(f.id, f);
|
||||
}
|
||||
});
|
||||
|
||||
const exportCiphers: any[] = [];
|
||||
decCiphers.forEach((c) => {
|
||||
// only export logins and secure notes
|
||||
if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) {
|
||||
return;
|
||||
}
|
||||
if (c.organizationId != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cipher: any = {};
|
||||
cipher.folder =
|
||||
c.folderId != null && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null;
|
||||
cipher.favorite = c.favorite ? 1 : null;
|
||||
this.buildCommonCipher(cipher, c);
|
||||
exportCiphers.push(cipher);
|
||||
});
|
||||
|
||||
return papa.unparse(exportCiphers);
|
||||
} else {
|
||||
const jsonDoc: any = {
|
||||
encrypted: false,
|
||||
folders: [],
|
||||
items: [],
|
||||
};
|
||||
|
||||
decFolders.forEach((f) => {
|
||||
if (f.id == null) {
|
||||
return;
|
||||
}
|
||||
const folder = new FolderExport();
|
||||
folder.build(f);
|
||||
jsonDoc.folders.push(folder);
|
||||
});
|
||||
|
||||
decCiphers.forEach((c) => {
|
||||
if (c.organizationId != null) {
|
||||
return;
|
||||
}
|
||||
const cipher = new CipherExport();
|
||||
cipher.build(c);
|
||||
cipher.collectionIds = null;
|
||||
jsonDoc.items.push(cipher);
|
||||
});
|
||||
|
||||
return JSON.stringify(jsonDoc, null, " ");
|
||||
}
|
||||
}
|
||||
|
||||
private async getEncryptedExport(): Promise<string> {
|
||||
let folders: Folder[] = [];
|
||||
let ciphers: Cipher[] = [];
|
||||
const promises = [];
|
||||
|
||||
promises.push(
|
||||
this.folderService.getAll().then((f) => {
|
||||
folders = f;
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.cipherService.getAll().then((c) => {
|
||||
ciphers = c.filter((f) => f.deletedDate == null);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid());
|
||||
|
||||
const jsonDoc: any = {
|
||||
encrypted: true,
|
||||
encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString,
|
||||
folders: [],
|
||||
items: [],
|
||||
};
|
||||
|
||||
folders.forEach((f) => {
|
||||
if (f.id == null) {
|
||||
return;
|
||||
}
|
||||
const folder = new FolderExport();
|
||||
folder.build(f);
|
||||
jsonDoc.folders.push(folder);
|
||||
});
|
||||
|
||||
ciphers.forEach((c) => {
|
||||
if (c.organizationId != null) {
|
||||
return;
|
||||
}
|
||||
const cipher = new CipherExport();
|
||||
cipher.build(c);
|
||||
cipher.collectionIds = null;
|
||||
jsonDoc.items.push(cipher);
|
||||
});
|
||||
|
||||
return JSON.stringify(jsonDoc, null, " ");
|
||||
}
|
||||
|
||||
private async getOrganizationDecryptedExport(
|
||||
organizationId: string,
|
||||
format: "json" | "csv"
|
||||
): Promise<string> {
|
||||
const decCollections: CollectionView[] = [];
|
||||
const decCiphers: CipherView[] = [];
|
||||
const promises = [];
|
||||
|
||||
promises.push(
|
||||
this.apiService.getCollections(organizationId).then((collections) => {
|
||||
const collectionPromises: any = [];
|
||||
if (collections != null && collections.data != null && collections.data.length > 0) {
|
||||
collections.data.forEach((c) => {
|
||||
const collection = new Collection(new CollectionData(c as CollectionDetailsResponse));
|
||||
collectionPromises.push(
|
||||
collection.decrypt().then((decCol) => {
|
||||
decCollections.push(decCol);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
return Promise.all(collectionPromises);
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.apiService.getCiphersOrganization(organizationId).then((ciphers) => {
|
||||
const cipherPromises: any = [];
|
||||
if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) {
|
||||
ciphers.data
|
||||
.filter((c) => c.deletedDate === null)
|
||||
.forEach((c) => {
|
||||
const cipher = new Cipher(new CipherData(c));
|
||||
cipherPromises.push(
|
||||
cipher.decrypt().then((decCipher) => {
|
||||
decCiphers.push(decCipher);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
return Promise.all(cipherPromises);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (format === "csv") {
|
||||
const collectionsMap = new Map<string, CollectionView>();
|
||||
decCollections.forEach((c) => {
|
||||
collectionsMap.set(c.id, c);
|
||||
});
|
||||
|
||||
const exportCiphers: any[] = [];
|
||||
decCiphers.forEach((c) => {
|
||||
// only export logins and secure notes
|
||||
if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cipher: any = {};
|
||||
cipher.collections = [];
|
||||
if (c.collectionIds != null) {
|
||||
cipher.collections = c.collectionIds
|
||||
.filter((id) => collectionsMap.has(id))
|
||||
.map((id) => collectionsMap.get(id).name);
|
||||
}
|
||||
this.buildCommonCipher(cipher, c);
|
||||
exportCiphers.push(cipher);
|
||||
});
|
||||
|
||||
return papa.unparse(exportCiphers);
|
||||
} else {
|
||||
const jsonDoc: any = {
|
||||
encrypted: false,
|
||||
collections: [],
|
||||
items: [],
|
||||
};
|
||||
|
||||
decCollections.forEach((c) => {
|
||||
const collection = new CollectionExport();
|
||||
collection.build(c);
|
||||
jsonDoc.collections.push(collection);
|
||||
});
|
||||
|
||||
decCiphers.forEach((c) => {
|
||||
const cipher = new CipherExport();
|
||||
cipher.build(c);
|
||||
jsonDoc.items.push(cipher);
|
||||
});
|
||||
return JSON.stringify(jsonDoc, null, " ");
|
||||
}
|
||||
}
|
||||
|
||||
private async getOrganizationEncryptedExport(organizationId: string): Promise<string> {
|
||||
const collections: Collection[] = [];
|
||||
const ciphers: Cipher[] = [];
|
||||
const promises = [];
|
||||
|
||||
promises.push(
|
||||
this.apiService.getCollections(organizationId).then((c) => {
|
||||
const collectionPromises: any = [];
|
||||
if (c != null && c.data != null && c.data.length > 0) {
|
||||
c.data.forEach((r) => {
|
||||
const collection = new Collection(new CollectionData(r as CollectionDetailsResponse));
|
||||
collections.push(collection);
|
||||
});
|
||||
}
|
||||
return Promise.all(collectionPromises);
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.apiService.getCiphersOrganization(organizationId).then((c) => {
|
||||
const cipherPromises: any = [];
|
||||
if (c != null && c.data != null && c.data.length > 0) {
|
||||
c.data
|
||||
.filter((item) => item.deletedDate === null)
|
||||
.forEach((item) => {
|
||||
const cipher = new Cipher(new CipherData(item));
|
||||
ciphers.push(cipher);
|
||||
});
|
||||
}
|
||||
return Promise.all(cipherPromises);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const orgKey = await this.cryptoService.getOrgKey(organizationId);
|
||||
const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), orgKey);
|
||||
|
||||
const jsonDoc: any = {
|
||||
encrypted: true,
|
||||
encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString,
|
||||
collections: [],
|
||||
items: [],
|
||||
};
|
||||
|
||||
collections.forEach((c) => {
|
||||
const collection = new CollectionExport();
|
||||
collection.build(c);
|
||||
jsonDoc.collections.push(collection);
|
||||
});
|
||||
|
||||
ciphers.forEach((c) => {
|
||||
const cipher = new CipherExport();
|
||||
cipher.build(c);
|
||||
jsonDoc.items.push(cipher);
|
||||
});
|
||||
return JSON.stringify(jsonDoc, null, " ");
|
||||
}
|
||||
|
||||
private padNumber(num: number, width: number, padCharacter = "0"): string {
|
||||
const numString = num.toString();
|
||||
return numString.length >= width
|
||||
? numString
|
||||
: new Array(width - numString.length + 1).join(padCharacter) + numString;
|
||||
}
|
||||
|
||||
private buildCommonCipher(cipher: any, c: CipherView) {
|
||||
cipher.type = null;
|
||||
cipher.name = c.name;
|
||||
cipher.notes = c.notes;
|
||||
cipher.fields = null;
|
||||
cipher.reprompt = c.reprompt;
|
||||
// Login props
|
||||
cipher.login_uri = null;
|
||||
cipher.login_username = null;
|
||||
cipher.login_password = null;
|
||||
cipher.login_totp = null;
|
||||
|
||||
if (c.fields) {
|
||||
c.fields.forEach((f: any) => {
|
||||
if (!cipher.fields) {
|
||||
cipher.fields = "";
|
||||
} else {
|
||||
cipher.fields += "\n";
|
||||
}
|
||||
|
||||
cipher.fields += (f.name || "") + ": " + f.value;
|
||||
});
|
||||
}
|
||||
|
||||
switch (c.type) {
|
||||
case CipherType.Login:
|
||||
cipher.type = "login";
|
||||
cipher.login_username = c.login.username;
|
||||
cipher.login_password = c.login.password;
|
||||
cipher.login_totp = c.login.totp;
|
||||
|
||||
if (c.login.uris) {
|
||||
cipher.login_uri = [];
|
||||
c.login.uris.forEach((u) => {
|
||||
cipher.login_uri.push(u.uri);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
cipher.type = "note";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
return cipher;
|
||||
}
|
||||
}
|
||||
108
libs/common/src/services/fileUpload.service.ts
Normal file
108
libs/common/src/services/fileUpload.service.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
193
libs/common/src/services/folder.service.ts
Normal file
193
libs/common/src/services/folder.service.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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(folders?: FolderView[]): Promise<TreeNode<FolderView>[]> {
|
||||
folders = 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 data = new FolderData(response);
|
||||
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);
|
||||
}
|
||||
}
|
||||
175
libs/common/src/services/i18n.service.ts
Normal file
175
libs/common/src/services/i18n.service.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { I18nService as I18nServiceAbstraction } from "../abstractions/i18n.service";
|
||||
|
||||
export class I18nService implements I18nServiceAbstraction {
|
||||
locale: string;
|
||||
// First locale is the default (English)
|
||||
supportedTranslationLocales: string[] = ["en"];
|
||||
translationLocale: string;
|
||||
collator: Intl.Collator;
|
||||
localeNames = new Map<string, string>([
|
||||
["af", "Afrikaans"],
|
||||
["az", "Azərbaycanca"],
|
||||
["be", "Беларуская"],
|
||||
["bg", "български"],
|
||||
["bn", "বাংলা"],
|
||||
["bs", "bosanski jezik"],
|
||||
["ca", "català"],
|
||||
["cs", "čeština"],
|
||||
["da", "dansk"],
|
||||
["de", "Deutsch"],
|
||||
["el", "Ελληνικά"],
|
||||
["en", "English"],
|
||||
["en-GB", "English (British)"],
|
||||
["en-IN", "English (India)"],
|
||||
["eo", "Esperanto"],
|
||||
["es", "español"],
|
||||
["et", "eesti"],
|
||||
["fa", "فارسی"],
|
||||
["fi", "suomi"],
|
||||
["fil", "Wikang Filipino"],
|
||||
["fr", "français"],
|
||||
["he", "עברית"],
|
||||
["hi", "हिन्दी"],
|
||||
["hr", "hrvatski"],
|
||||
["hu", "magyar"],
|
||||
["id", "Bahasa Indonesia"],
|
||||
["it", "italiano"],
|
||||
["ja", "日本語"],
|
||||
["ka", "ქართული"],
|
||||
["km", "ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ"],
|
||||
["kn", "ಕನ್ನಡ"],
|
||||
["ko", "한국어"],
|
||||
["lt", "lietuvių kalba"],
|
||||
["lv", "Latvietis"],
|
||||
["me", "црногорски"],
|
||||
["ml", "മലയാളം"],
|
||||
["nb", "norsk (bokmål)"],
|
||||
["nl", "Nederlands"],
|
||||
["nn", "Norsk Nynorsk"],
|
||||
["pl", "polski"],
|
||||
["pt-BR", "português do Brasil"],
|
||||
["pt-PT", "português"],
|
||||
["ro", "română"],
|
||||
["ru", "русский"],
|
||||
["si", "සිංහල"],
|
||||
["sk", "slovenčina"],
|
||||
["sl", "Slovenski jezik, Slovenščina"],
|
||||
["sr", "Српски"],
|
||||
["sv", "svenska"],
|
||||
["th", "ไทย"],
|
||||
["tr", "Türkçe"],
|
||||
["uk", "українська"],
|
||||
["vi", "Tiếng Việt"],
|
||||
["zh-CN", "中文(中国大陆)"],
|
||||
["zh-TW", "中文(台灣)"],
|
||||
]);
|
||||
|
||||
protected inited: boolean;
|
||||
protected defaultMessages: any = {};
|
||||
protected localeMessages: any = {};
|
||||
|
||||
constructor(
|
||||
protected systemLanguage: string,
|
||||
protected localesDirectory: string,
|
||||
protected getLocalesJson: (formattedLocale: string) => Promise<any>
|
||||
) {
|
||||
this.systemLanguage = systemLanguage.replace("_", "-");
|
||||
}
|
||||
|
||||
async init(locale?: string) {
|
||||
if (this.inited) {
|
||||
throw new Error("i18n already initialized.");
|
||||
}
|
||||
if (this.supportedTranslationLocales == null || this.supportedTranslationLocales.length === 0) {
|
||||
throw new Error("supportedTranslationLocales not set.");
|
||||
}
|
||||
|
||||
this.inited = true;
|
||||
this.locale = this.translationLocale = locale != null ? locale : this.systemLanguage;
|
||||
|
||||
try {
|
||||
this.collator = new Intl.Collator(this.locale, { numeric: true, sensitivity: "base" });
|
||||
} catch {
|
||||
this.collator = null;
|
||||
}
|
||||
|
||||
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
|
||||
this.translationLocale = this.translationLocale.slice(0, 2);
|
||||
|
||||
if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) {
|
||||
this.translationLocale = this.supportedTranslationLocales[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.localesDirectory != null) {
|
||||
await this.loadMessages(this.translationLocale, this.localeMessages);
|
||||
if (this.translationLocale !== this.supportedTranslationLocales[0]) {
|
||||
await this.loadMessages(this.supportedTranslationLocales[0], this.defaultMessages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t(id: string, p1?: string, p2?: string, p3?: string): string {
|
||||
return this.translate(id, p1, p2, p3);
|
||||
}
|
||||
|
||||
translate(id: string, p1?: string, p2?: string, p3?: string): string {
|
||||
let result: string;
|
||||
// eslint-disable-next-line
|
||||
if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) {
|
||||
result = this.localeMessages[id];
|
||||
// eslint-disable-next-line
|
||||
} else if (this.defaultMessages.hasOwnProperty(id) && this.defaultMessages[id]) {
|
||||
result = this.defaultMessages[id];
|
||||
} else {
|
||||
result = "";
|
||||
}
|
||||
|
||||
if (result !== "") {
|
||||
if (p1 != null) {
|
||||
result = result.split("__$1__").join(p1);
|
||||
}
|
||||
if (p2 != null) {
|
||||
result = result.split("__$2__").join(p2);
|
||||
}
|
||||
if (p3 != null) {
|
||||
result = result.split("__$3__").join(p3);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async loadMessages(locale: string, messagesObj: any): Promise<any> {
|
||||
const formattedLocale = locale.replace("-", "_");
|
||||
const locales = await this.getLocalesJson(formattedLocale);
|
||||
for (const prop in locales) {
|
||||
// eslint-disable-next-line
|
||||
if (!locales.hasOwnProperty(prop)) {
|
||||
continue;
|
||||
}
|
||||
messagesObj[prop] = locales[prop].message;
|
||||
|
||||
if (locales[prop].placeholders) {
|
||||
for (const placeProp in locales[prop].placeholders) {
|
||||
if (
|
||||
!locales[prop].placeholders.hasOwnProperty(placeProp) || // eslint-disable-line
|
||||
!locales[prop].placeholders[placeProp].content
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const replaceToken = "\\$" + placeProp.toUpperCase() + "\\$";
|
||||
let replaceContent = locales[prop].placeholders[placeProp].content;
|
||||
if (replaceContent === "$1" || replaceContent === "$2" || replaceContent === "$3") {
|
||||
replaceContent = "__$" + replaceContent + "__";
|
||||
}
|
||||
messagesObj[prop] = messagesObj[prop].replace(
|
||||
new RegExp(replaceToken, "g"),
|
||||
replaceContent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
382
libs/common/src/services/import.service.ts
Normal file
382
libs/common/src/services/import.service.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { CipherService } from "../abstractions/cipher.service";
|
||||
import { CollectionService } from "../abstractions/collection.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { FolderService } from "../abstractions/folder.service";
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
import { ImportService as ImportServiceAbstraction } from "../abstractions/import.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||
import { CipherType } from "../enums/cipherType";
|
||||
import {
|
||||
featuredImportOptions,
|
||||
ImportOption,
|
||||
ImportType,
|
||||
regularImportOptions,
|
||||
} from "../enums/importOptions";
|
||||
import { AscendoCsvImporter } from "../importers/ascendoCsvImporter";
|
||||
import { AvastCsvImporter } from "../importers/avastCsvImporter";
|
||||
import { AvastJsonImporter } from "../importers/avastJsonImporter";
|
||||
import { AviraCsvImporter } from "../importers/aviraCsvImporter";
|
||||
import { BitwardenCsvImporter } from "../importers/bitwardenCsvImporter";
|
||||
import { BitwardenJsonImporter } from "../importers/bitwardenJsonImporter";
|
||||
import { BitwardenPasswordProtectedImporter } from "../importers/bitwardenPasswordProtectedImporter";
|
||||
import { BlackBerryCsvImporter } from "../importers/blackBerryCsvImporter";
|
||||
import { BlurCsvImporter } from "../importers/blurCsvImporter";
|
||||
import { ButtercupCsvImporter } from "../importers/buttercupCsvImporter";
|
||||
import { ChromeCsvImporter } from "../importers/chromeCsvImporter";
|
||||
import { ClipperzHtmlImporter } from "../importers/clipperzHtmlImporter";
|
||||
import { CodebookCsvImporter } from "../importers/codebookCsvImporter";
|
||||
import { DashlaneCsvImporter } from "../importers/dashlaneImporters/dashlaneCsvImporter";
|
||||
import { DashlaneJsonImporter } from "../importers/dashlaneImporters/dashlaneJsonImporter";
|
||||
import { EncryptrCsvImporter } from "../importers/encryptrCsvImporter";
|
||||
import { EnpassCsvImporter } from "../importers/enpassCsvImporter";
|
||||
import { EnpassJsonImporter } from "../importers/enpassJsonImporter";
|
||||
import { FirefoxCsvImporter } from "../importers/firefoxCsvImporter";
|
||||
import { FSecureFskImporter } from "../importers/fsecureFskImporter";
|
||||
import { GnomeJsonImporter } from "../importers/gnomeJsonImporter";
|
||||
import { ImportError } from "../importers/importError";
|
||||
import { Importer } from "../importers/importer";
|
||||
import { KasperskyTxtImporter } from "../importers/kasperskyTxtImporter";
|
||||
import { KeePass2XmlImporter } from "../importers/keepass2XmlImporter";
|
||||
import { KeePassXCsvImporter } from "../importers/keepassxCsvImporter";
|
||||
import { KeeperCsvImporter } from "../importers/keeperImporters/keeperCsvImporter";
|
||||
import { LastPassCsvImporter } from "../importers/lastpassCsvImporter";
|
||||
import { LogMeOnceCsvImporter } from "../importers/logMeOnceCsvImporter";
|
||||
import { MeldiumCsvImporter } from "../importers/meldiumCsvImporter";
|
||||
import { MSecureCsvImporter } from "../importers/msecureCsvImporter";
|
||||
import { MykiCsvImporter } from "../importers/mykiCsvImporter";
|
||||
import { NordPassCsvImporter } from "../importers/nordpassCsvImporter";
|
||||
import { OnePassword1PifImporter } from "../importers/onepasswordImporters/onepassword1PifImporter";
|
||||
import { OnePassword1PuxImporter } from "../importers/onepasswordImporters/onepassword1PuxImporter";
|
||||
import { OnePasswordMacCsvImporter } from "../importers/onepasswordImporters/onepasswordMacCsvImporter";
|
||||
import { OnePasswordWinCsvImporter } from "../importers/onepasswordImporters/onepasswordWinCsvImporter";
|
||||
import { PadlockCsvImporter } from "../importers/padlockCsvImporter";
|
||||
import { PassKeepCsvImporter } from "../importers/passkeepCsvImporter";
|
||||
import { PassmanJsonImporter } from "../importers/passmanJsonImporter";
|
||||
import { PasspackCsvImporter } from "../importers/passpackCsvImporter";
|
||||
import { PasswordAgentCsvImporter } from "../importers/passwordAgentCsvImporter";
|
||||
import { PasswordBossJsonImporter } from "../importers/passwordBossJsonImporter";
|
||||
import { PasswordDragonXmlImporter } from "../importers/passwordDragonXmlImporter";
|
||||
import { PasswordSafeXmlImporter } from "../importers/passwordSafeXmlImporter";
|
||||
import { PasswordWalletTxtImporter } from "../importers/passwordWalletTxtImporter";
|
||||
import { RememBearCsvImporter } from "../importers/rememBearCsvImporter";
|
||||
import { RoboFormCsvImporter } from "../importers/roboformCsvImporter";
|
||||
import { SafariCsvImporter } from "../importers/safariCsvImporter";
|
||||
import { SafeInCloudXmlImporter } from "../importers/safeInCloudXmlImporter";
|
||||
import { SaferPassCsvImporter } from "../importers/saferpassCsvImport";
|
||||
import { SecureSafeCsvImporter } from "../importers/secureSafeCsvImporter";
|
||||
import { SplashIdCsvImporter } from "../importers/splashIdCsvImporter";
|
||||
import { StickyPasswordXmlImporter } from "../importers/stickyPasswordXmlImporter";
|
||||
import { TrueKeyCsvImporter } from "../importers/truekeyCsvImporter";
|
||||
import { UpmCsvImporter } from "../importers/upmCsvImporter";
|
||||
import { YotiCsvImporter } from "../importers/yotiCsvImporter";
|
||||
import { ZohoVaultCsvImporter } from "../importers/zohoVaultCsvImporter";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { ImportResult } from "../models/domain/importResult";
|
||||
import { CipherRequest } from "../models/request/cipherRequest";
|
||||
import { CollectionRequest } from "../models/request/collectionRequest";
|
||||
import { FolderRequest } from "../models/request/folderRequest";
|
||||
import { ImportCiphersRequest } from "../models/request/importCiphersRequest";
|
||||
import { ImportOrganizationCiphersRequest } from "../models/request/importOrganizationCiphersRequest";
|
||||
import { KvpRequest } from "../models/request/kvpRequest";
|
||||
import { ErrorResponse } from "../models/response/errorResponse";
|
||||
import { CipherView } from "../models/view/cipherView";
|
||||
|
||||
export class ImportService implements ImportServiceAbstraction {
|
||||
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
|
||||
|
||||
regularImportOptions = regularImportOptions as readonly ImportOption[];
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private folderService: FolderService,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private collectionService: CollectionService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cryptoService: CryptoService
|
||||
) {}
|
||||
|
||||
getImportOptions(): ImportOption[] {
|
||||
return this.featuredImportOptions.concat(this.regularImportOptions);
|
||||
}
|
||||
|
||||
async import(
|
||||
importer: Importer,
|
||||
fileContents: string,
|
||||
organizationId: string = null
|
||||
): Promise<ImportError> {
|
||||
const importResult = await importer.parse(fileContents);
|
||||
if (importResult.success) {
|
||||
if (importResult.folders.length === 0 && importResult.ciphers.length === 0) {
|
||||
return new ImportError(this.i18nService.t("importNothingError"));
|
||||
} else if (importResult.ciphers.length > 0) {
|
||||
const halfway = Math.floor(importResult.ciphers.length / 2);
|
||||
const last = importResult.ciphers.length - 1;
|
||||
|
||||
if (
|
||||
this.badData(importResult.ciphers[0]) &&
|
||||
this.badData(importResult.ciphers[halfway]) &&
|
||||
this.badData(importResult.ciphers[last])
|
||||
) {
|
||||
return new ImportError(this.i18nService.t("importFormatError"));
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.postImport(importResult, organizationId);
|
||||
} catch (error) {
|
||||
const errorResponse = new ErrorResponse(error, 400);
|
||||
return this.handleServerError(errorResponse, importResult);
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
if (!Utils.isNullOrWhitespace(importResult.errorMessage)) {
|
||||
return new ImportError(importResult.errorMessage, importResult.missingPassword);
|
||||
} else {
|
||||
return new ImportError(
|
||||
this.i18nService.t("importFormatError"),
|
||||
importResult.missingPassword
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getImporter(
|
||||
format: ImportType | "bitwardenpasswordprotected",
|
||||
organizationId: string = null,
|
||||
password: string = null
|
||||
): Importer {
|
||||
const importer = this.getImporterInstance(format, password);
|
||||
if (importer == null) {
|
||||
return null;
|
||||
}
|
||||
importer.organizationId = organizationId;
|
||||
return importer;
|
||||
}
|
||||
|
||||
private getImporterInstance(format: ImportType | "bitwardenpasswordprotected", password: string) {
|
||||
if (format == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case "bitwardencsv":
|
||||
return new BitwardenCsvImporter();
|
||||
case "bitwardenjson":
|
||||
return new BitwardenJsonImporter(this.cryptoService, this.i18nService);
|
||||
case "bitwardenpasswordprotected":
|
||||
return new BitwardenPasswordProtectedImporter(
|
||||
this.cryptoService,
|
||||
this.i18nService,
|
||||
password
|
||||
);
|
||||
case "lastpasscsv":
|
||||
case "passboltcsv":
|
||||
return new LastPassCsvImporter();
|
||||
case "keepassxcsv":
|
||||
return new KeePassXCsvImporter();
|
||||
case "aviracsv":
|
||||
return new AviraCsvImporter();
|
||||
case "blurcsv":
|
||||
return new BlurCsvImporter();
|
||||
case "safeincloudxml":
|
||||
return new SafeInCloudXmlImporter();
|
||||
case "padlockcsv":
|
||||
return new PadlockCsvImporter();
|
||||
case "keepass2xml":
|
||||
return new KeePass2XmlImporter();
|
||||
case "chromecsv":
|
||||
case "operacsv":
|
||||
case "vivaldicsv":
|
||||
return new ChromeCsvImporter();
|
||||
case "firefoxcsv":
|
||||
return new FirefoxCsvImporter();
|
||||
case "upmcsv":
|
||||
return new UpmCsvImporter();
|
||||
case "saferpasscsv":
|
||||
return new SaferPassCsvImporter();
|
||||
case "safaricsv":
|
||||
return new SafariCsvImporter();
|
||||
case "meldiumcsv":
|
||||
return new MeldiumCsvImporter();
|
||||
case "1password1pif":
|
||||
return new OnePassword1PifImporter();
|
||||
case "1password1pux":
|
||||
return new OnePassword1PuxImporter();
|
||||
case "1passwordwincsv":
|
||||
return new OnePasswordWinCsvImporter();
|
||||
case "1passwordmaccsv":
|
||||
return new OnePasswordMacCsvImporter();
|
||||
case "keepercsv":
|
||||
return new KeeperCsvImporter();
|
||||
// case "keeperjson":
|
||||
// return new KeeperJsonImporter();
|
||||
case "passworddragonxml":
|
||||
return new PasswordDragonXmlImporter();
|
||||
case "enpasscsv":
|
||||
return new EnpassCsvImporter();
|
||||
case "enpassjson":
|
||||
return new EnpassJsonImporter();
|
||||
case "pwsafexml":
|
||||
return new PasswordSafeXmlImporter();
|
||||
case "dashlanecsv":
|
||||
return new DashlaneCsvImporter();
|
||||
case "dashlanejson":
|
||||
return new DashlaneJsonImporter();
|
||||
case "msecurecsv":
|
||||
return new MSecureCsvImporter();
|
||||
case "stickypasswordxml":
|
||||
return new StickyPasswordXmlImporter();
|
||||
case "truekeycsv":
|
||||
return new TrueKeyCsvImporter();
|
||||
case "clipperzhtml":
|
||||
return new ClipperzHtmlImporter();
|
||||
case "roboformcsv":
|
||||
return new RoboFormCsvImporter();
|
||||
case "ascendocsv":
|
||||
return new AscendoCsvImporter();
|
||||
case "passwordbossjson":
|
||||
return new PasswordBossJsonImporter();
|
||||
case "zohovaultcsv":
|
||||
return new ZohoVaultCsvImporter();
|
||||
case "splashidcsv":
|
||||
return new SplashIdCsvImporter();
|
||||
case "passkeepcsv":
|
||||
return new PassKeepCsvImporter();
|
||||
case "gnomejson":
|
||||
return new GnomeJsonImporter();
|
||||
case "passwordagentcsv":
|
||||
return new PasswordAgentCsvImporter();
|
||||
case "passpackcsv":
|
||||
return new PasspackCsvImporter();
|
||||
case "passmanjson":
|
||||
return new PassmanJsonImporter();
|
||||
case "avastcsv":
|
||||
return new AvastCsvImporter();
|
||||
case "avastjson":
|
||||
return new AvastJsonImporter();
|
||||
case "fsecurefsk":
|
||||
return new FSecureFskImporter();
|
||||
case "kasperskytxt":
|
||||
return new KasperskyTxtImporter();
|
||||
case "remembearcsv":
|
||||
return new RememBearCsvImporter();
|
||||
case "passwordwallettxt":
|
||||
return new PasswordWalletTxtImporter();
|
||||
case "mykicsv":
|
||||
return new MykiCsvImporter();
|
||||
case "securesafecsv":
|
||||
return new SecureSafeCsvImporter();
|
||||
case "logmeoncecsv":
|
||||
return new LogMeOnceCsvImporter();
|
||||
case "blackberrycsv":
|
||||
return new BlackBerryCsvImporter();
|
||||
case "buttercupcsv":
|
||||
return new ButtercupCsvImporter();
|
||||
case "codebookcsv":
|
||||
return new CodebookCsvImporter();
|
||||
case "encryptrcsv":
|
||||
return new EncryptrCsvImporter();
|
||||
case "yoticsv":
|
||||
return new YotiCsvImporter();
|
||||
case "nordpasscsv":
|
||||
return new NordPassCsvImporter();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async postImport(importResult: ImportResult, organizationId: string = null) {
|
||||
if (organizationId == null) {
|
||||
const request = new ImportCiphersRequest();
|
||||
for (let i = 0; i < importResult.ciphers.length; i++) {
|
||||
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
|
||||
request.ciphers.push(new CipherRequest(c));
|
||||
}
|
||||
if (importResult.folders != null) {
|
||||
for (let i = 0; i < importResult.folders.length; i++) {
|
||||
const f = await this.folderService.encrypt(importResult.folders[i]);
|
||||
request.folders.push(new FolderRequest(f));
|
||||
}
|
||||
}
|
||||
if (importResult.folderRelationships != null) {
|
||||
importResult.folderRelationships.forEach((r) =>
|
||||
request.folderRelationships.push(new KvpRequest(r[0], r[1]))
|
||||
);
|
||||
}
|
||||
return await this.apiService.postImportCiphers(request);
|
||||
} else {
|
||||
const request = new ImportOrganizationCiphersRequest();
|
||||
for (let i = 0; i < importResult.ciphers.length; i++) {
|
||||
importResult.ciphers[i].organizationId = organizationId;
|
||||
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
|
||||
request.ciphers.push(new CipherRequest(c));
|
||||
}
|
||||
if (importResult.collections != null) {
|
||||
for (let i = 0; i < importResult.collections.length; i++) {
|
||||
importResult.collections[i].organizationId = organizationId;
|
||||
const c = await this.collectionService.encrypt(importResult.collections[i]);
|
||||
request.collections.push(new CollectionRequest(c));
|
||||
}
|
||||
}
|
||||
if (importResult.collectionRelationships != null) {
|
||||
importResult.collectionRelationships.forEach((r) =>
|
||||
request.collectionRelationships.push(new KvpRequest(r[0], r[1]))
|
||||
);
|
||||
}
|
||||
return await this.apiService.postImportOrganizationCiphers(organizationId, request);
|
||||
}
|
||||
}
|
||||
|
||||
private badData(c: CipherView) {
|
||||
return (
|
||||
(c.name == null || c.name === "--") &&
|
||||
c.type === CipherType.Login &&
|
||||
c.login != null &&
|
||||
Utils.isNullOrWhitespace(c.login.password)
|
||||
);
|
||||
}
|
||||
|
||||
private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): ImportError {
|
||||
if (errorResponse.validationErrors == null) {
|
||||
return new ImportError(errorResponse.message);
|
||||
}
|
||||
|
||||
let errorMessage = "";
|
||||
|
||||
Object.entries(errorResponse.validationErrors).forEach(([key, value], index) => {
|
||||
let item;
|
||||
let itemType;
|
||||
const i = Number(key.match(/[0-9]+/)[0]);
|
||||
|
||||
switch (key.match(/^\w+/)[0]) {
|
||||
case "Ciphers":
|
||||
item = importResult.ciphers[i];
|
||||
itemType = CipherType[item.type];
|
||||
break;
|
||||
case "Folders":
|
||||
item = importResult.folders[i];
|
||||
itemType = "Folder";
|
||||
break;
|
||||
case "Collections":
|
||||
item = importResult.collections[i];
|
||||
itemType = "Collection";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
errorMessage += "\n\n";
|
||||
}
|
||||
|
||||
if (itemType !== "Folder" && itemType !== "Collection") {
|
||||
errorMessage += "[" + (i + 1) + "] ";
|
||||
}
|
||||
|
||||
errorMessage += "[" + itemType + '] "' + item.name + '": ' + value;
|
||||
});
|
||||
|
||||
return new ImportError(errorMessage);
|
||||
}
|
||||
}
|
||||
142
libs/common/src/services/keyConnector.service.ts
Normal file
142
libs/common/src/services/keyConnector.service.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { OrganizationService } from "../abstractions/organization.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { OrganizationUserType } from "../enums/organizationUserType";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
|
||||
import { KeysRequest } from "../models/request/keysRequest";
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private cryptoService: CryptoService,
|
||||
private apiService: ApiService,
|
||||
private tokenService: TokenService,
|
||||
private logService: LogService,
|
||||
private organizationService: OrganizationService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private logoutCallback: (expired: boolean, userId?: string) => void
|
||||
) {}
|
||||
|
||||
setUsesKeyConnector(usesKeyConnector: boolean) {
|
||||
return this.stateService.setUsesKeyConnector(usesKeyConnector);
|
||||
}
|
||||
|
||||
async getUsesKeyConnector(): Promise<boolean> {
|
||||
return await this.stateService.getUsesKeyConnector();
|
||||
}
|
||||
|
||||
async userNeedsMigration() {
|
||||
const loggedInUsingSso = await this.tokenService.getIsExternal();
|
||||
const requiredByOrganization = (await this.getManagingOrganization()) != null;
|
||||
const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector());
|
||||
|
||||
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
|
||||
}
|
||||
|
||||
async migrateUser() {
|
||||
const organization = await this.getManagingOrganization();
|
||||
const key = await this.cryptoService.getKey();
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
|
||||
|
||||
try {
|
||||
await this.apiService.postUserKeyToKeyConnector(
|
||||
organization.keyConnectorUrl,
|
||||
keyConnectorRequest
|
||||
);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
|
||||
await this.apiService.postConvertToKeyConnector();
|
||||
}
|
||||
|
||||
async getAndSetKey(url: string) {
|
||||
try {
|
||||
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
|
||||
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
|
||||
const k = new SymmetricCryptoKey(keyArr);
|
||||
await this.cryptoService.setKey(k);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async getManagingOrganization() {
|
||||
const orgs = await this.organizationService.getAll();
|
||||
return orgs.find(
|
||||
(o) =>
|
||||
o.keyConnectorEnabled &&
|
||||
o.type !== OrganizationUserType.Admin &&
|
||||
o.type !== OrganizationUserType.Owner &&
|
||||
!o.isProviderUser
|
||||
);
|
||||
}
|
||||
|
||||
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
|
||||
const { kdf, kdfIterations, keyConnectorUrl } = tokenResponse;
|
||||
const password = await this.cryptoFunctionService.randomBytes(64);
|
||||
|
||||
const k = await this.cryptoService.makeKey(
|
||||
Utils.fromBufferToB64(password),
|
||||
await this.tokenService.getEmail(),
|
||||
kdf,
|
||||
kdfIterations
|
||||
);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64);
|
||||
await this.cryptoService.setKey(k);
|
||||
|
||||
const encKey = await this.cryptoService.makeEncKey(k);
|
||||
await this.cryptoService.setEncKey(encKey[1].encryptedString);
|
||||
|
||||
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
||||
|
||||
try {
|
||||
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
|
||||
const keys = new KeysRequest(pubKey, privKey.encryptedString);
|
||||
const setPasswordRequest = new SetKeyConnectorKeyRequest(
|
||||
encKey[1].encryptedString,
|
||||
kdf,
|
||||
kdfIterations,
|
||||
orgId,
|
||||
keys
|
||||
);
|
||||
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
|
||||
}
|
||||
|
||||
async setConvertAccountRequired(status: boolean) {
|
||||
await this.stateService.setConvertAccountToKeyConnector(status);
|
||||
}
|
||||
|
||||
async getConvertAccountRequired(): Promise<boolean> {
|
||||
return await this.stateService.getConvertAccountToKeyConnector();
|
||||
}
|
||||
|
||||
async removeConvertAccountRequired() {
|
||||
await this.stateService.setConvertAccountToKeyConnector(null);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.removeConvertAccountRequired();
|
||||
}
|
||||
|
||||
private handleKeyConnectorError(e: any) {
|
||||
this.logService.error(e);
|
||||
if (this.logoutCallback != null) {
|
||||
this.logoutCallback(false);
|
||||
}
|
||||
throw new Error("Key Connector error");
|
||||
}
|
||||
}
|
||||
7
libs/common/src/services/noopMessaging.service.ts
Normal file
7
libs/common/src/services/noopMessaging.service.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { MessagingService } from "../abstractions/messaging.service";
|
||||
|
||||
export class NoopMessagingService implements MessagingService {
|
||||
send(subscriber: string, arg: any = {}) {
|
||||
// Do nothing...
|
||||
}
|
||||
}
|
||||
229
libs/common/src/services/notifications.service.ts
Normal file
229
libs/common/src/services/notifications.service.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
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 { AuthService } from "../abstractions/auth.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 { AuthenticationStatus } from "../enums/authenticationStatus";
|
||||
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 environmentService: EnvironmentService,
|
||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
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(true);
|
||||
}
|
||||
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() {
|
||||
const authStatus = await this.authService.getAuthStatus();
|
||||
return authStatus >= AuthenticationStatus.Unlocked;
|
||||
}
|
||||
|
||||
private random(min: number, max: number) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
55
libs/common/src/services/organization.service.ts
Normal file
55
libs/common/src/services/organization.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { OrganizationData } from "../models/data/organizationData";
|
||||
import { Organization } from "../models/domain/organization";
|
||||
|
||||
export class OrganizationService implements OrganizationServiceAbstraction {
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
async get(id: string): Promise<Organization> {
|
||||
const organizations = await this.stateService.getOrganizations();
|
||||
// eslint-disable-next-line
|
||||
if (organizations == null || !organizations.hasOwnProperty(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Organization(organizations[id]);
|
||||
}
|
||||
|
||||
async getByIdentifier(identifier: string): Promise<Organization> {
|
||||
const organizations = await this.getAll();
|
||||
if (organizations == null || organizations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return organizations.find((o) => o.identifier === identifier);
|
||||
}
|
||||
|
||||
async getAll(userId?: string): Promise<Organization[]> {
|
||||
const organizations = await this.stateService.getOrganizations({ userId: userId });
|
||||
const response: Organization[] = [];
|
||||
for (const id in organizations) {
|
||||
// eslint-disable-next-line
|
||||
if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) {
|
||||
response.push(new Organization(organizations[id]));
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async save(organizations: { [id: string]: OrganizationData }) {
|
||||
return await this.stateService.setOrganizations(organizations);
|
||||
}
|
||||
|
||||
async canManageSponsorships(): Promise<boolean> {
|
||||
const orgs = await this.getAll();
|
||||
return orgs.some(
|
||||
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null
|
||||
);
|
||||
}
|
||||
|
||||
async hasOrganizations(userId?: string): Promise<boolean> {
|
||||
const organizations = await this.getAll(userId);
|
||||
return organizations.length > 0;
|
||||
}
|
||||
}
|
||||
572
libs/common/src/services/passwordGeneration.service.ts
Normal file
572
libs/common/src/services/passwordGeneration.service.ts
Normal file
@@ -0,0 +1,572 @@
|
||||
import * as zxcvbn from "zxcvbn";
|
||||
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "../abstractions/passwordGeneration.service";
|
||||
import { PolicyService } from "../abstractions/policy.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { PolicyType } from "../enums/policyType";
|
||||
import { EEFLongWordList } from "../misc/wordlist";
|
||||
import { EncString } from "../models/domain/encString";
|
||||
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
|
||||
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
|
||||
import { Policy } from "../models/domain/policy";
|
||||
|
||||
const DefaultOptions = {
|
||||
length: 14,
|
||||
ambiguous: false,
|
||||
number: true,
|
||||
minNumber: 1,
|
||||
uppercase: true,
|
||||
minUppercase: 0,
|
||||
lowercase: true,
|
||||
minLowercase: 0,
|
||||
special: false,
|
||||
minSpecial: 1,
|
||||
type: "password",
|
||||
numWords: 3,
|
||||
wordSeparator: "-",
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
};
|
||||
|
||||
const MaxPasswordsInHistory = 100;
|
||||
|
||||
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private policyService: PolicyService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async generatePassword(options: any): Promise<string> {
|
||||
// overload defaults with given options
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
if (o.type === "passphrase") {
|
||||
return this.generatePassphrase(options);
|
||||
}
|
||||
|
||||
// sanitize
|
||||
this.sanitizePasswordLength(o, true);
|
||||
|
||||
const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial;
|
||||
if (o.length < minLength) {
|
||||
o.length = minLength;
|
||||
}
|
||||
|
||||
const positions: string[] = [];
|
||||
if (o.lowercase && o.minLowercase > 0) {
|
||||
for (let i = 0; i < o.minLowercase; i++) {
|
||||
positions.push("l");
|
||||
}
|
||||
}
|
||||
if (o.uppercase && o.minUppercase > 0) {
|
||||
for (let i = 0; i < o.minUppercase; i++) {
|
||||
positions.push("u");
|
||||
}
|
||||
}
|
||||
if (o.number && o.minNumber > 0) {
|
||||
for (let i = 0; i < o.minNumber; i++) {
|
||||
positions.push("n");
|
||||
}
|
||||
}
|
||||
if (o.special && o.minSpecial > 0) {
|
||||
for (let i = 0; i < o.minSpecial; i++) {
|
||||
positions.push("s");
|
||||
}
|
||||
}
|
||||
while (positions.length < o.length) {
|
||||
positions.push("a");
|
||||
}
|
||||
|
||||
// shuffle
|
||||
await this.shuffleArray(positions);
|
||||
|
||||
// build out the char sets
|
||||
let allCharSet = "";
|
||||
|
||||
let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz";
|
||||
if (o.ambiguous) {
|
||||
lowercaseCharSet += "l";
|
||||
}
|
||||
if (o.lowercase) {
|
||||
allCharSet += lowercaseCharSet;
|
||||
}
|
||||
|
||||
let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
||||
if (o.ambiguous) {
|
||||
uppercaseCharSet += "IO";
|
||||
}
|
||||
if (o.uppercase) {
|
||||
allCharSet += uppercaseCharSet;
|
||||
}
|
||||
|
||||
let numberCharSet = "23456789";
|
||||
if (o.ambiguous) {
|
||||
numberCharSet += "01";
|
||||
}
|
||||
if (o.number) {
|
||||
allCharSet += numberCharSet;
|
||||
}
|
||||
|
||||
const specialCharSet = "!@#$%^&*";
|
||||
if (o.special) {
|
||||
allCharSet += specialCharSet;
|
||||
}
|
||||
|
||||
let password = "";
|
||||
for (let i = 0; i < o.length; i++) {
|
||||
let positionChars: string;
|
||||
switch (positions[i]) {
|
||||
case "l":
|
||||
positionChars = lowercaseCharSet;
|
||||
break;
|
||||
case "u":
|
||||
positionChars = uppercaseCharSet;
|
||||
break;
|
||||
case "n":
|
||||
positionChars = numberCharSet;
|
||||
break;
|
||||
case "s":
|
||||
positionChars = specialCharSet;
|
||||
break;
|
||||
case "a":
|
||||
positionChars = allCharSet;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1);
|
||||
password += positionChars.charAt(randomCharIndex);
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
async generatePassphrase(options: any): Promise<string> {
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
if (o.numWords == null || o.numWords <= 2) {
|
||||
o.numWords = DefaultOptions.numWords;
|
||||
}
|
||||
if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) {
|
||||
o.wordSeparator = " ";
|
||||
}
|
||||
if (o.capitalize == null) {
|
||||
o.capitalize = false;
|
||||
}
|
||||
if (o.includeNumber == null) {
|
||||
o.includeNumber = false;
|
||||
}
|
||||
|
||||
const listLength = EEFLongWordList.length - 1;
|
||||
const wordList = new Array(o.numWords);
|
||||
for (let i = 0; i < o.numWords; i++) {
|
||||
const wordIndex = await this.cryptoService.randomNumber(0, listLength);
|
||||
if (o.capitalize) {
|
||||
wordList[i] = this.capitalize(EEFLongWordList[wordIndex]);
|
||||
} else {
|
||||
wordList[i] = EEFLongWordList[wordIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (o.includeNumber) {
|
||||
await this.appendRandomNumberToRandomWord(wordList);
|
||||
}
|
||||
return wordList.join(o.wordSeparator);
|
||||
}
|
||||
|
||||
async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> {
|
||||
let options = await this.stateService.getPasswordGenerationOptions();
|
||||
if (options == null) {
|
||||
options = Object.assign({}, DefaultOptions);
|
||||
} else {
|
||||
options = Object.assign({}, DefaultOptions, options);
|
||||
}
|
||||
await this.stateService.setPasswordGenerationOptions(options);
|
||||
const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(options);
|
||||
options = enforcedOptions[0];
|
||||
return [options, enforcedOptions[1]];
|
||||
}
|
||||
|
||||
async enforcePasswordGeneratorPoliciesOnOptions(
|
||||
options: any
|
||||
): Promise<[any, PasswordGeneratorPolicyOptions]> {
|
||||
let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions();
|
||||
if (enforcedPolicyOptions != null) {
|
||||
if (options.length < enforcedPolicyOptions.minLength) {
|
||||
options.length = enforcedPolicyOptions.minLength;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useUppercase) {
|
||||
options.uppercase = true;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useLowercase) {
|
||||
options.lowercase = true;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useNumbers) {
|
||||
options.number = true;
|
||||
}
|
||||
|
||||
if (options.minNumber < enforcedPolicyOptions.numberCount) {
|
||||
options.minNumber = enforcedPolicyOptions.numberCount;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.useSpecial) {
|
||||
options.special = true;
|
||||
}
|
||||
|
||||
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
|
||||
options.minSpecial = enforcedPolicyOptions.specialCount;
|
||||
}
|
||||
|
||||
// Must normalize these fields because the receiving call expects all options to pass the current rules
|
||||
if (options.minSpecial + options.minNumber > options.length) {
|
||||
options.minSpecial = options.length - options.minNumber;
|
||||
}
|
||||
|
||||
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
|
||||
options.numWords = enforcedPolicyOptions.minNumberWords;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.capitalize) {
|
||||
options.capitalize = true;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.includeNumber) {
|
||||
options.includeNumber = true;
|
||||
}
|
||||
|
||||
// Force default type if password/passphrase selected via policy
|
||||
if (
|
||||
enforcedPolicyOptions.defaultType === "password" ||
|
||||
enforcedPolicyOptions.defaultType === "passphrase"
|
||||
) {
|
||||
options.type = enforcedPolicyOptions.defaultType;
|
||||
}
|
||||
} else {
|
||||
// UI layer expects an instantiated object to prevent more explicit null checks
|
||||
enforcedPolicyOptions = new PasswordGeneratorPolicyOptions();
|
||||
}
|
||||
return [options, enforcedPolicyOptions];
|
||||
}
|
||||
|
||||
async getPasswordGeneratorPolicyOptions(): Promise<PasswordGeneratorPolicyOptions> {
|
||||
const policies: Policy[] =
|
||||
this.policyService == null
|
||||
? null
|
||||
: await this.policyService.getAll(PolicyType.PasswordGenerator);
|
||||
let enforcedOptions: PasswordGeneratorPolicyOptions = null;
|
||||
|
||||
if (policies == null || policies.length === 0) {
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
policies.forEach((currentPolicy) => {
|
||||
if (!currentPolicy.enabled || currentPolicy.data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enforcedOptions == null) {
|
||||
enforcedOptions = new PasswordGeneratorPolicyOptions();
|
||||
}
|
||||
|
||||
// Password wins in multi-org collisions
|
||||
if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== "password") {
|
||||
enforcedOptions.defaultType = currentPolicy.data.defaultType;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minLength != null &&
|
||||
currentPolicy.data.minLength > enforcedOptions.minLength
|
||||
) {
|
||||
enforcedOptions.minLength = currentPolicy.data.minLength;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useUpper) {
|
||||
enforcedOptions.useUppercase = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useLower) {
|
||||
enforcedOptions.useLowercase = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useNumbers) {
|
||||
enforcedOptions.useNumbers = true;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minNumbers != null &&
|
||||
currentPolicy.data.minNumbers > enforcedOptions.numberCount
|
||||
) {
|
||||
enforcedOptions.numberCount = currentPolicy.data.minNumbers;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.useSpecial) {
|
||||
enforcedOptions.useSpecial = true;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minSpecial != null &&
|
||||
currentPolicy.data.minSpecial > enforcedOptions.specialCount
|
||||
) {
|
||||
enforcedOptions.specialCount = currentPolicy.data.minSpecial;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minNumberWords != null &&
|
||||
currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords
|
||||
) {
|
||||
enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.capitalize) {
|
||||
enforcedOptions.capitalize = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.includeNumber) {
|
||||
enforcedOptions.includeNumber = true;
|
||||
}
|
||||
});
|
||||
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
async saveOptions(options: any) {
|
||||
await this.stateService.setPasswordGenerationOptions(options);
|
||||
}
|
||||
|
||||
async getHistory(): Promise<GeneratedPasswordHistory[]> {
|
||||
const hasKey = await this.cryptoService.hasKey();
|
||||
if (!hasKey) {
|
||||
return new Array<GeneratedPasswordHistory>();
|
||||
}
|
||||
|
||||
if ((await this.stateService.getDecryptedPasswordGenerationHistory()) == null) {
|
||||
const encrypted = await this.stateService.getEncryptedPasswordGenerationHistory();
|
||||
const decrypted = await this.decryptHistory(encrypted);
|
||||
await this.stateService.setDecryptedPasswordGenerationHistory(decrypted);
|
||||
}
|
||||
|
||||
const passwordGenerationHistory =
|
||||
await this.stateService.getDecryptedPasswordGenerationHistory();
|
||||
return passwordGenerationHistory != null
|
||||
? passwordGenerationHistory
|
||||
: new Array<GeneratedPasswordHistory>();
|
||||
}
|
||||
|
||||
async addHistory(password: string): Promise<any> {
|
||||
// Cannot add new history if no key is available
|
||||
const hasKey = await this.cryptoService.hasKey();
|
||||
if (!hasKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHistory = await this.getHistory();
|
||||
|
||||
// Prevent duplicates
|
||||
if (this.matchesPrevious(password, currentHistory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now()));
|
||||
|
||||
// Remove old items.
|
||||
if (currentHistory.length > MaxPasswordsInHistory) {
|
||||
currentHistory.pop();
|
||||
}
|
||||
|
||||
const newHistory = await this.encryptHistory(currentHistory);
|
||||
return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId });
|
||||
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
||||
}
|
||||
|
||||
passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult {
|
||||
if (password == null || password.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let globalUserInputs = ["bitwarden", "bit", "warden"];
|
||||
if (userInputs != null && userInputs.length > 0) {
|
||||
globalUserInputs = globalUserInputs.concat(userInputs);
|
||||
}
|
||||
// Use a hash set to get rid of any duplicate user inputs
|
||||
const finalUserInputs = Array.from(new Set(globalUserInputs));
|
||||
const result = zxcvbn(password, finalUserInputs);
|
||||
return result;
|
||||
}
|
||||
|
||||
normalizeOptions(options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) {
|
||||
options.minLowercase = 0;
|
||||
options.minUppercase = 0;
|
||||
|
||||
if (!options.length || options.length < 5) {
|
||||
options.length = 5;
|
||||
} else if (options.length > 128) {
|
||||
options.length = 128;
|
||||
}
|
||||
|
||||
if (options.length < enforcedPolicyOptions.minLength) {
|
||||
options.length = enforcedPolicyOptions.minLength;
|
||||
}
|
||||
|
||||
if (!options.minNumber) {
|
||||
options.minNumber = 0;
|
||||
} else if (options.minNumber > options.length) {
|
||||
options.minNumber = options.length;
|
||||
} else if (options.minNumber > 9) {
|
||||
options.minNumber = 9;
|
||||
}
|
||||
|
||||
if (options.minNumber < enforcedPolicyOptions.numberCount) {
|
||||
options.minNumber = enforcedPolicyOptions.numberCount;
|
||||
}
|
||||
|
||||
if (!options.minSpecial) {
|
||||
options.minSpecial = 0;
|
||||
} else if (options.minSpecial > options.length) {
|
||||
options.minSpecial = options.length;
|
||||
} else if (options.minSpecial > 9) {
|
||||
options.minSpecial = 9;
|
||||
}
|
||||
|
||||
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
|
||||
options.minSpecial = enforcedPolicyOptions.specialCount;
|
||||
}
|
||||
|
||||
if (options.minSpecial + options.minNumber > options.length) {
|
||||
options.minSpecial = options.length - options.minNumber;
|
||||
}
|
||||
|
||||
if (options.numWords == null || options.length < 3) {
|
||||
options.numWords = 3;
|
||||
} else if (options.numWords > 20) {
|
||||
options.numWords = 20;
|
||||
}
|
||||
|
||||
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
|
||||
options.numWords = enforcedPolicyOptions.minNumberWords;
|
||||
}
|
||||
|
||||
if (options.wordSeparator != null && options.wordSeparator.length > 1) {
|
||||
options.wordSeparator = options.wordSeparator[0];
|
||||
}
|
||||
|
||||
this.sanitizePasswordLength(options, false);
|
||||
}
|
||||
|
||||
private capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
private async appendRandomNumberToRandomWord(wordList: string[]) {
|
||||
if (wordList == null || wordList.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const index = await this.cryptoService.randomNumber(0, wordList.length - 1);
|
||||
const num = await this.cryptoService.randomNumber(0, 9);
|
||||
wordList[index] = wordList[index] + num;
|
||||
}
|
||||
|
||||
private async encryptHistory(
|
||||
history: GeneratedPasswordHistory[]
|
||||
): Promise<GeneratedPasswordHistory[]> {
|
||||
if (history == null || history.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const promises = history.map(async (item) => {
|
||||
const encrypted = await this.cryptoService.encrypt(item.password);
|
||||
return new GeneratedPasswordHistory(encrypted.encryptedString, item.date);
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async decryptHistory(
|
||||
history: GeneratedPasswordHistory[]
|
||||
): Promise<GeneratedPasswordHistory[]> {
|
||||
if (history == null || history.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const promises = history.map(async (item) => {
|
||||
const decrypted = await this.cryptoService.decryptToUtf8(new EncString(item.password));
|
||||
return new GeneratedPasswordHistory(decrypted, item.date);
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean {
|
||||
if (history == null || history.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return history[history.length - 1].password === password;
|
||||
}
|
||||
|
||||
// ref: https://stackoverflow.com/a/12646864/1090359
|
||||
private async shuffleArray(array: string[]) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = await this.cryptoService.randomNumber(0, i);
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizePasswordLength(options: any, forGeneration: boolean) {
|
||||
let minUppercaseCalc = 0;
|
||||
let minLowercaseCalc = 0;
|
||||
let minNumberCalc: number = options.minNumber;
|
||||
let minSpecialCalc: number = options.minSpecial;
|
||||
|
||||
if (options.uppercase && options.minUppercase <= 0) {
|
||||
minUppercaseCalc = 1;
|
||||
} else if (!options.uppercase) {
|
||||
minUppercaseCalc = 0;
|
||||
}
|
||||
|
||||
if (options.lowercase && options.minLowercase <= 0) {
|
||||
minLowercaseCalc = 1;
|
||||
} else if (!options.lowercase) {
|
||||
minLowercaseCalc = 0;
|
||||
}
|
||||
|
||||
if (options.number && options.minNumber <= 0) {
|
||||
minNumberCalc = 1;
|
||||
} else if (!options.number) {
|
||||
minNumberCalc = 0;
|
||||
}
|
||||
|
||||
if (options.special && options.minSpecial <= 0) {
|
||||
minSpecialCalc = 1;
|
||||
} else if (!options.special) {
|
||||
minSpecialCalc = 0;
|
||||
}
|
||||
|
||||
// This should never happen but is a final safety net
|
||||
if (!options.length || options.length < 1) {
|
||||
options.length = 10;
|
||||
}
|
||||
|
||||
const minLength: number = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc;
|
||||
// Normalize and Generation both require this modification
|
||||
if (options.length < minLength) {
|
||||
options.length = minLength;
|
||||
}
|
||||
|
||||
// Apply other changes if the options object passed in is for generation
|
||||
if (forGeneration) {
|
||||
options.minUppercase = minUppercaseCalc;
|
||||
options.minLowercase = minLowercaseCalc;
|
||||
options.minNumber = minNumberCalc;
|
||||
options.minSpecial = minSpecialCalc;
|
||||
}
|
||||
}
|
||||
}
|
||||
247
libs/common/src/services/policy.service.ts
Normal file
247
libs/common/src/services/policy.service.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { OrganizationService } from "../abstractions/organization.service";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "../abstractions/policy.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { OrganizationUserStatusType } from "../enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "../enums/organizationUserType";
|
||||
import { PolicyType } from "../enums/policyType";
|
||||
import { PolicyData } from "../models/data/policyData";
|
||||
import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions";
|
||||
import { Organization } from "../models/domain/organization";
|
||||
import { Policy } from "../models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions";
|
||||
import { ListResponse } from "../models/response/listResponse";
|
||||
import { PolicyResponse } from "../models/response/policyResponse";
|
||||
|
||||
export class PolicyService implements PolicyServiceAbstraction {
|
||||
policyCache: Policy[];
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
private apiService: ApiService
|
||||
) {}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
await this.stateService.setDecryptedPolicies(null);
|
||||
}
|
||||
|
||||
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
|
||||
let response: Policy[] = [];
|
||||
const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId });
|
||||
if (decryptedPolicies != null) {
|
||||
response = decryptedPolicies;
|
||||
} else {
|
||||
const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId });
|
||||
for (const id in diskPolicies) {
|
||||
// eslint-disable-next-line
|
||||
if (diskPolicies.hasOwnProperty(id)) {
|
||||
response.push(new Policy(diskPolicies[id]));
|
||||
}
|
||||
}
|
||||
await this.stateService.setDecryptedPolicies(response, { userId: userId });
|
||||
}
|
||||
if (type != null) {
|
||||
return response.filter((policy) => policy.type === type);
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise<Policy> {
|
||||
const org = await this.organizationService.get(organizationId);
|
||||
if (org?.isProviderUser) {
|
||||
const orgPolicies = await this.apiService.getPolicies(organizationId);
|
||||
const policy = orgPolicies.data.find((p) => p.organizationId === organizationId);
|
||||
|
||||
if (policy == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Policy(new PolicyData(policy));
|
||||
}
|
||||
|
||||
const policies = await this.getAll(policyType);
|
||||
return policies.find((p) => p.organizationId === organizationId);
|
||||
}
|
||||
|
||||
async replace(policies: { [id: string]: PolicyData }): Promise<any> {
|
||||
await this.stateService.setDecryptedPolicies(null);
|
||||
await this.stateService.setEncryptedPolicies(policies);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
await this.stateService.setDecryptedPolicies(null, { userId: userId });
|
||||
await this.stateService.setEncryptedPolicies(null, { userId: userId });
|
||||
}
|
||||
|
||||
async getMasterPasswordPoliciesForInvitedUsers(
|
||||
orgId: string
|
||||
): Promise<MasterPasswordPolicyOptions> {
|
||||
const userId = await this.stateService.getUserId();
|
||||
const response = await this.apiService.getPoliciesByInvitedUser(orgId, userId);
|
||||
const policies = await this.mapPoliciesFromToken(response);
|
||||
return this.getMasterPasswordPolicyOptions(policies);
|
||||
}
|
||||
|
||||
async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise<MasterPasswordPolicyOptions> {
|
||||
let enforcedOptions: MasterPasswordPolicyOptions = null;
|
||||
|
||||
if (policies == null) {
|
||||
policies = await this.getAll(PolicyType.MasterPassword);
|
||||
} else {
|
||||
policies = policies.filter((p) => p.type === PolicyType.MasterPassword);
|
||||
}
|
||||
|
||||
if (policies == null || policies.length === 0) {
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
policies.forEach((currentPolicy) => {
|
||||
if (!currentPolicy.enabled || currentPolicy.data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enforcedOptions == null) {
|
||||
enforcedOptions = new MasterPasswordPolicyOptions();
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minComplexity != null &&
|
||||
currentPolicy.data.minComplexity > enforcedOptions.minComplexity
|
||||
) {
|
||||
enforcedOptions.minComplexity = currentPolicy.data.minComplexity;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPolicy.data.minLength != null &&
|
||||
currentPolicy.data.minLength > enforcedOptions.minLength
|
||||
) {
|
||||
enforcedOptions.minLength = currentPolicy.data.minLength;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireUpper) {
|
||||
enforcedOptions.requireUpper = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireLower) {
|
||||
enforcedOptions.requireLower = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireNumbers) {
|
||||
enforcedOptions.requireNumbers = true;
|
||||
}
|
||||
|
||||
if (currentPolicy.data.requireSpecial) {
|
||||
enforcedOptions.requireSpecial = true;
|
||||
}
|
||||
});
|
||||
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
evaluateMasterPassword(
|
||||
passwordStrength: number,
|
||||
newPassword: string,
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions
|
||||
): boolean {
|
||||
if (enforcedPolicyOptions == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions.minComplexity > 0 &&
|
||||
enforcedPolicyOptions.minComplexity > passwordStrength
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions.minLength > 0 &&
|
||||
enforcedPolicyOptions.minLength > newPassword.length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enforcedPolicyOptions.requireNumbers && !/[0-9]/.test(newPassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getResetPasswordPolicyOptions(
|
||||
policies: Policy[],
|
||||
orgId: string
|
||||
): [ResetPasswordPolicyOptions, boolean] {
|
||||
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
|
||||
|
||||
if (policies == null || orgId == null) {
|
||||
return [resetPasswordPolicyOptions, false];
|
||||
}
|
||||
|
||||
const policy = policies.find(
|
||||
(p) => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled
|
||||
);
|
||||
resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false;
|
||||
|
||||
return [resetPasswordPolicyOptions, policy?.enabled ?? false];
|
||||
}
|
||||
|
||||
mapPoliciesFromToken(policiesResponse: ListResponse<PolicyResponse>): Policy[] {
|
||||
if (policiesResponse == null || policiesResponse.data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const policiesData = policiesResponse.data.map((p) => new PolicyData(p));
|
||||
return policiesData.map((p) => new Policy(p));
|
||||
}
|
||||
|
||||
async policyAppliesToUser(
|
||||
policyType: PolicyType,
|
||||
policyFilter?: (policy: Policy) => boolean,
|
||||
userId?: string
|
||||
) {
|
||||
const policies = await this.getAll(policyType, userId);
|
||||
const organizations = await this.organizationService.getAll(userId);
|
||||
let filteredPolicies;
|
||||
|
||||
if (policyFilter != null) {
|
||||
filteredPolicies = policies.filter((p) => p.enabled && policyFilter(p));
|
||||
} else {
|
||||
filteredPolicies = policies.filter((p) => p.enabled);
|
||||
}
|
||||
|
||||
const policySet = new Set(filteredPolicies.map((p) => p.organizationId));
|
||||
|
||||
return organizations.some(
|
||||
(o) =>
|
||||
o.enabled &&
|
||||
o.status >= OrganizationUserStatusType.Accepted &&
|
||||
o.usePolicies &&
|
||||
!this.isExcemptFromPolicies(o, policyType) &&
|
||||
policySet.has(o.id)
|
||||
);
|
||||
}
|
||||
|
||||
private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) {
|
||||
if (policyType === PolicyType.MaximumVaultTimeout) {
|
||||
return organization.type === OrganizationUserType.Owner;
|
||||
}
|
||||
|
||||
return organization.isExemptFromPolicies;
|
||||
}
|
||||
}
|
||||
34
libs/common/src/services/provider.service.ts
Normal file
34
libs/common/src/services/provider.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
308
libs/common/src/services/search.service.ts
Normal file
308
libs/common/src/services/search.service.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
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;
|
||||
}
|
||||
//register lunr pipeline function
|
||||
lunr.Pipeline.registerFunction(this.normalizeAccentsPipelineFunction, "normalizeAccents");
|
||||
}
|
||||
|
||||
clearIndex(): void {
|
||||
this.indexedEntityId = null;
|
||||
this.index = null;
|
||||
}
|
||||
|
||||
isSearchable(query: string): boolean {
|
||||
query = SearchService.normalizeSearchQuery(query);
|
||||
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.pipeline.add(this.normalizeAccentsPipelineFunction);
|
||||
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 = SearchService.normalizeSearchQuery(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 = SearchService.normalizeSearchQuery(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 = SearchService.normalizeSearchQuery(query.trim().toLocaleLowerCase());
|
||||
if (query === null) {
|
||||
return sends;
|
||||
}
|
||||
const sendsMatched: SendView[] = [];
|
||||
const lowPriorityMatched: SendView[] = [];
|
||||
sends.forEach((s) => {
|
||||
if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) {
|
||||
sendsMatched.push(s);
|
||||
} else if (
|
||||
query.length >= 8 &&
|
||||
(s.id.startsWith(query) ||
|
||||
s.accessId.toLocaleLowerCase().startsWith(query) ||
|
||||
(s.file?.id != null && s.file.id.startsWith(query)))
|
||||
) {
|
||||
lowPriorityMatched.push(s);
|
||||
} else if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) {
|
||||
lowPriorityMatched.push(s);
|
||||
} else if (s.text?.text != null && s.text.text.toLowerCase().indexOf(query) > -1) {
|
||||
lowPriorityMatched.push(s);
|
||||
} else if (s.file?.fileName != null && s.file.fileName.toLowerCase().indexOf(query) > -1) {
|
||||
lowPriorityMatched.push(s);
|
||||
}
|
||||
});
|
||||
return sendsMatched.concat(lowPriorityMatched);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private normalizeAccentsPipelineFunction(token: lunr.Token): any {
|
||||
const searchableFields = ["name", "login.username", "subtitle", "notes"];
|
||||
const fields = (token as any).metadata["fields"];
|
||||
const checkFields = fields.every((i: any) => searchableFields.includes(i));
|
||||
|
||||
if (checkFields) {
|
||||
return SearchService.normalizeSearchQuery(token.toString());
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// Remove accents/diacritics characters from text. This regex is equivalent to the Diacritic unicode property escape, i.e. it will match all diacritic characters.
|
||||
static normalizeSearchQuery(query: string): string {
|
||||
return query?.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||
}
|
||||
}
|
||||
295
libs/common/src/services/send.service.ts
Normal file
295
libs/common/src/services/send.service.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
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 data = new SendData(response);
|
||||
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 data = new SendData(response);
|
||||
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];
|
||||
}
|
||||
}
|
||||
56
libs/common/src/services/settings.service.ts
Normal file
56
libs/common/src/services/settings.service.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
2534
libs/common/src/services/state.service.ts
Normal file
2534
libs/common/src/services/state.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
513
libs/common/src/services/stateMigration.service.ts
Normal file
513
libs/common/src/services/stateMigration.service.ts
Normal file
@@ -0,0 +1,513 @@
|
||||
import { StorageService } from "../abstractions/storage.service";
|
||||
import { HtmlStorageLocation } from "../enums/htmlStorageLocation";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { StateVersion } from "../enums/stateVersion";
|
||||
import { ThemeType } from "../enums/themeType";
|
||||
import { StateFactory } from "../factories/stateFactory";
|
||||
import { CipherData } from "../models/data/cipherData";
|
||||
import { CollectionData } from "../models/data/collectionData";
|
||||
import { EventData } from "../models/data/eventData";
|
||||
import { FolderData } from "../models/data/folderData";
|
||||
import { OrganizationData } from "../models/data/organizationData";
|
||||
import { PolicyData } from "../models/data/policyData";
|
||||
import { ProviderData } from "../models/data/providerData";
|
||||
import { SendData } from "../models/data/sendData";
|
||||
import { Account, AccountSettings } from "../models/domain/account";
|
||||
import { EnvironmentUrls } from "../models/domain/environmentUrls";
|
||||
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
|
||||
import { GlobalState } from "../models/domain/globalState";
|
||||
import { StorageOptions } from "../models/domain/storageOptions";
|
||||
|
||||
import { TokenService } from "./token.service";
|
||||
|
||||
// Originally (before January 2022) storage was handled as a flat key/value pair store.
|
||||
// With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration.
|
||||
const v1Keys: { [key: string]: string } = {
|
||||
accessToken: "accessToken",
|
||||
alwaysShowDock: "alwaysShowDock",
|
||||
autoConfirmFingerprints: "autoConfirmFingerprints",
|
||||
autoFillOnPageLoadDefault: "autoFillOnPageLoadDefault",
|
||||
biometricAwaitingAcceptance: "biometricAwaitingAcceptance",
|
||||
biometricFingerprintValidated: "biometricFingerprintValidated",
|
||||
biometricText: "biometricText",
|
||||
biometricUnlock: "biometric",
|
||||
clearClipboard: "clearClipboardKey",
|
||||
clientId: "apikey_clientId",
|
||||
clientSecret: "apikey_clientSecret",
|
||||
collapsedGroupings: "collapsedGroupings",
|
||||
convertAccountToKeyConnector: "convertAccountToKeyConnector",
|
||||
defaultUriMatch: "defaultUriMatch",
|
||||
disableAddLoginNotification: "disableAddLoginNotification",
|
||||
disableAutoBiometricsPrompt: "noAutoPromptBiometrics",
|
||||
disableAutoTotpCopy: "disableAutoTotpCopy",
|
||||
disableBadgeCounter: "disableBadgeCounter",
|
||||
disableChangedPasswordNotification: "disableChangedPasswordNotification",
|
||||
disableContextMenuItem: "disableContextMenuItem",
|
||||
disableFavicon: "disableFavicon",
|
||||
disableGa: "disableGa",
|
||||
dontShowCardsCurrentTab: "dontShowCardsCurrentTab",
|
||||
dontShowIdentitiesCurrentTab: "dontShowIdentitiesCurrentTab",
|
||||
emailVerified: "emailVerified",
|
||||
enableAlwaysOnTop: "enableAlwaysOnTopKey",
|
||||
enableAutoFillOnPageLoad: "enableAutoFillOnPageLoad",
|
||||
enableBiometric: "enabledBiometric",
|
||||
enableBrowserIntegration: "enableBrowserIntegration",
|
||||
enableBrowserIntegrationFingerprint: "enableBrowserIntegrationFingerprint",
|
||||
enableCloseToTray: "enableCloseToTray",
|
||||
enableFullWidth: "enableFullWidth",
|
||||
enableGravatars: "enableGravatars",
|
||||
enableMinimizeToTray: "enableMinimizeToTray",
|
||||
enableStartToTray: "enableStartToTrayKey",
|
||||
enableTray: "enableTray",
|
||||
encKey: "encKey", // Generated Symmetric Key
|
||||
encOrgKeys: "encOrgKeys",
|
||||
encPrivate: "encPrivateKey",
|
||||
encProviderKeys: "encProviderKeys",
|
||||
entityId: "entityId",
|
||||
entityType: "entityType",
|
||||
environmentUrls: "environmentUrls",
|
||||
equivalentDomains: "equivalentDomains",
|
||||
eventCollection: "eventCollection",
|
||||
forcePasswordReset: "forcePasswordReset",
|
||||
history: "generatedPasswordHistory",
|
||||
installedVersion: "installedVersion",
|
||||
kdf: "kdf",
|
||||
kdfIterations: "kdfIterations",
|
||||
key: "key", // Master Key
|
||||
keyHash: "keyHash",
|
||||
lastActive: "lastActive",
|
||||
localData: "sitesLocalData",
|
||||
locale: "locale",
|
||||
mainWindowSize: "mainWindowSize",
|
||||
minimizeOnCopyToClipboard: "minimizeOnCopyToClipboardKey",
|
||||
neverDomains: "neverDomains",
|
||||
noAutoPromptBiometricsText: "noAutoPromptBiometricsText",
|
||||
openAtLogin: "openAtLogin",
|
||||
passwordGenerationOptions: "passwordGenerationOptions",
|
||||
pinProtected: "pinProtectedKey",
|
||||
protectedPin: "protectedPin",
|
||||
refreshToken: "refreshToken",
|
||||
ssoCodeVerifier: "ssoCodeVerifier",
|
||||
ssoIdentifier: "ssoOrgIdentifier",
|
||||
ssoState: "ssoState",
|
||||
stamp: "securityStamp",
|
||||
theme: "theme",
|
||||
userEmail: "userEmail",
|
||||
userId: "userId",
|
||||
usesConnector: "usesKeyConnector",
|
||||
vaultTimeoutAction: "vaultTimeoutAction",
|
||||
vaultTimeout: "lockOption",
|
||||
rememberedEmail: "rememberedEmail",
|
||||
};
|
||||
|
||||
const v1KeyPrefixes: { [key: string]: string } = {
|
||||
ciphers: "ciphers_",
|
||||
collections: "collections_",
|
||||
folders: "folders_",
|
||||
lastSync: "lastSync_",
|
||||
policies: "policies_",
|
||||
twoFactorToken: "twoFactorToken_",
|
||||
organizations: "organizations_",
|
||||
providers: "providers_",
|
||||
sends: "sends_",
|
||||
settings: "settings_",
|
||||
};
|
||||
|
||||
const keys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
activeUserId: "activeUserId",
|
||||
tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication
|
||||
accountActivity: "accountActivity",
|
||||
};
|
||||
|
||||
const partialKeys = {
|
||||
autoKey: "_masterkey_auto",
|
||||
biometricKey: "_masterkey_biometric",
|
||||
masterKey: "_masterkey",
|
||||
};
|
||||
|
||||
export class StateMigrationService<
|
||||
TGlobalState extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account
|
||||
> {
|
||||
constructor(
|
||||
protected storageService: StorageService,
|
||||
protected secureStorageService: StorageService,
|
||||
protected stateFactory: StateFactory<TGlobalState, TAccount>
|
||||
) {}
|
||||
|
||||
async needsMigration(): Promise<boolean> {
|
||||
const currentStateVersion = await this.getCurrentStateVersion();
|
||||
return currentStateVersion == null || currentStateVersion < StateVersion.Latest;
|
||||
}
|
||||
|
||||
async migrate(): Promise<void> {
|
||||
let currentStateVersion = await this.getCurrentStateVersion();
|
||||
while (currentStateVersion < StateVersion.Latest) {
|
||||
switch (currentStateVersion) {
|
||||
case StateVersion.One:
|
||||
await this.migrateStateFrom1To2();
|
||||
break;
|
||||
case StateVersion.Two:
|
||||
await this.migrateStateFrom2To3();
|
||||
break;
|
||||
case StateVersion.Three:
|
||||
await this.migrateStateFrom3To4();
|
||||
break;
|
||||
}
|
||||
|
||||
currentStateVersion += 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected async migrateStateFrom1To2(): Promise<void> {
|
||||
const clearV1Keys = async (clearingUserId?: string) => {
|
||||
for (const key in v1Keys) {
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
await this.set(v1Keys[key], null);
|
||||
}
|
||||
if (clearingUserId != null) {
|
||||
for (const keyPrefix in v1KeyPrefixes) {
|
||||
if (keyPrefix == null) {
|
||||
continue;
|
||||
}
|
||||
await this.set(v1KeyPrefixes[keyPrefix] + userId, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Some processes, like biometrics, may have already defined a value before migrations are run.
|
||||
// We don't want to null out those values if they don't exist in the old storage scheme (like for new installs)
|
||||
// So, the OOO for migration is that we:
|
||||
// 1. Check for an existing storage value from the old storage structure OR
|
||||
// 2. Check for a value already set by processes that run before migration OR
|
||||
// 3. Assign the default value
|
||||
const globals =
|
||||
(await this.get<GlobalState>(keys.global)) ?? this.stateFactory.createGlobal(null);
|
||||
globals.stateVersion = StateVersion.Two;
|
||||
globals.environmentUrls =
|
||||
(await this.get<EnvironmentUrls>(v1Keys.environmentUrls)) ?? globals.environmentUrls;
|
||||
globals.locale = (await this.get<string>(v1Keys.locale)) ?? globals.locale;
|
||||
globals.noAutoPromptBiometrics =
|
||||
(await this.get<boolean>(v1Keys.disableAutoBiometricsPrompt)) ??
|
||||
globals.noAutoPromptBiometrics;
|
||||
globals.noAutoPromptBiometricsText =
|
||||
(await this.get<string>(v1Keys.noAutoPromptBiometricsText)) ??
|
||||
globals.noAutoPromptBiometricsText;
|
||||
globals.ssoCodeVerifier =
|
||||
(await this.get<string>(v1Keys.ssoCodeVerifier)) ?? globals.ssoCodeVerifier;
|
||||
globals.ssoOrganizationIdentifier =
|
||||
(await this.get<string>(v1Keys.ssoIdentifier)) ?? globals.ssoOrganizationIdentifier;
|
||||
globals.ssoState = (await this.get<any>(v1Keys.ssoState)) ?? globals.ssoState;
|
||||
globals.rememberedEmail =
|
||||
(await this.get<string>(v1Keys.rememberedEmail)) ?? globals.rememberedEmail;
|
||||
globals.theme = (await this.get<ThemeType>(v1Keys.theme)) ?? globals.theme;
|
||||
globals.vaultTimeout = (await this.get<number>(v1Keys.vaultTimeout)) ?? globals.vaultTimeout;
|
||||
globals.vaultTimeoutAction =
|
||||
(await this.get<string>(v1Keys.vaultTimeoutAction)) ?? globals.vaultTimeoutAction;
|
||||
globals.window = (await this.get<any>(v1Keys.mainWindowSize)) ?? globals.window;
|
||||
globals.enableTray = (await this.get<boolean>(v1Keys.enableTray)) ?? globals.enableTray;
|
||||
globals.enableMinimizeToTray =
|
||||
(await this.get<boolean>(v1Keys.enableMinimizeToTray)) ?? globals.enableMinimizeToTray;
|
||||
globals.enableCloseToTray =
|
||||
(await this.get<boolean>(v1Keys.enableCloseToTray)) ?? globals.enableCloseToTray;
|
||||
globals.enableStartToTray =
|
||||
(await this.get<boolean>(v1Keys.enableStartToTray)) ?? globals.enableStartToTray;
|
||||
globals.openAtLogin = (await this.get<boolean>(v1Keys.openAtLogin)) ?? globals.openAtLogin;
|
||||
globals.alwaysShowDock =
|
||||
(await this.get<boolean>(v1Keys.alwaysShowDock)) ?? globals.alwaysShowDock;
|
||||
globals.enableBrowserIntegration =
|
||||
(await this.get<boolean>(v1Keys.enableBrowserIntegration)) ??
|
||||
globals.enableBrowserIntegration;
|
||||
globals.enableBrowserIntegrationFingerprint =
|
||||
(await this.get<boolean>(v1Keys.enableBrowserIntegrationFingerprint)) ??
|
||||
globals.enableBrowserIntegrationFingerprint;
|
||||
|
||||
const userId =
|
||||
(await this.get<string>(v1Keys.userId)) ?? (await this.get<string>(v1Keys.entityId));
|
||||
|
||||
const defaultAccount = this.stateFactory.createAccount(null);
|
||||
const accountSettings: AccountSettings = {
|
||||
autoConfirmFingerPrints:
|
||||
(await this.get<boolean>(v1Keys.autoConfirmFingerprints)) ??
|
||||
defaultAccount.settings.autoConfirmFingerPrints,
|
||||
autoFillOnPageLoadDefault:
|
||||
(await this.get<boolean>(v1Keys.autoFillOnPageLoadDefault)) ??
|
||||
defaultAccount.settings.autoFillOnPageLoadDefault,
|
||||
biometricLocked: null,
|
||||
biometricUnlock:
|
||||
(await this.get<boolean>(v1Keys.biometricUnlock)) ??
|
||||
defaultAccount.settings.biometricUnlock,
|
||||
clearClipboard:
|
||||
(await this.get<number>(v1Keys.clearClipboard)) ?? defaultAccount.settings.clearClipboard,
|
||||
defaultUriMatch:
|
||||
(await this.get<any>(v1Keys.defaultUriMatch)) ?? defaultAccount.settings.defaultUriMatch,
|
||||
disableAddLoginNotification:
|
||||
(await this.get<boolean>(v1Keys.disableAddLoginNotification)) ??
|
||||
defaultAccount.settings.disableAddLoginNotification,
|
||||
disableAutoBiometricsPrompt:
|
||||
(await this.get<boolean>(v1Keys.disableAutoBiometricsPrompt)) ??
|
||||
defaultAccount.settings.disableAutoBiometricsPrompt,
|
||||
disableAutoTotpCopy:
|
||||
(await this.get<boolean>(v1Keys.disableAutoTotpCopy)) ??
|
||||
defaultAccount.settings.disableAutoTotpCopy,
|
||||
disableBadgeCounter:
|
||||
(await this.get<boolean>(v1Keys.disableBadgeCounter)) ??
|
||||
defaultAccount.settings.disableBadgeCounter,
|
||||
disableChangedPasswordNotification:
|
||||
(await this.get<boolean>(v1Keys.disableChangedPasswordNotification)) ??
|
||||
defaultAccount.settings.disableChangedPasswordNotification,
|
||||
disableContextMenuItem:
|
||||
(await this.get<boolean>(v1Keys.disableContextMenuItem)) ??
|
||||
defaultAccount.settings.disableContextMenuItem,
|
||||
disableGa: (await this.get<boolean>(v1Keys.disableGa)) ?? defaultAccount.settings.disableGa,
|
||||
dontShowCardsCurrentTab:
|
||||
(await this.get<boolean>(v1Keys.dontShowCardsCurrentTab)) ??
|
||||
defaultAccount.settings.dontShowCardsCurrentTab,
|
||||
dontShowIdentitiesCurrentTab:
|
||||
(await this.get<boolean>(v1Keys.dontShowIdentitiesCurrentTab)) ??
|
||||
defaultAccount.settings.dontShowIdentitiesCurrentTab,
|
||||
enableAlwaysOnTop:
|
||||
(await this.get<boolean>(v1Keys.enableAlwaysOnTop)) ??
|
||||
defaultAccount.settings.enableAlwaysOnTop,
|
||||
enableAutoFillOnPageLoad:
|
||||
(await this.get<boolean>(v1Keys.enableAutoFillOnPageLoad)) ??
|
||||
defaultAccount.settings.enableAutoFillOnPageLoad,
|
||||
enableBiometric:
|
||||
(await this.get<boolean>(v1Keys.enableBiometric)) ??
|
||||
defaultAccount.settings.enableBiometric,
|
||||
enableFullWidth:
|
||||
(await this.get<boolean>(v1Keys.enableFullWidth)) ??
|
||||
defaultAccount.settings.enableFullWidth,
|
||||
enableGravitars:
|
||||
(await this.get<boolean>(v1Keys.enableGravatars)) ??
|
||||
defaultAccount.settings.enableGravitars,
|
||||
environmentUrls: globals.environmentUrls ?? defaultAccount.settings.environmentUrls,
|
||||
equivalentDomains:
|
||||
(await this.get<any>(v1Keys.equivalentDomains)) ??
|
||||
defaultAccount.settings.equivalentDomains,
|
||||
minimizeOnCopyToClipboard:
|
||||
(await this.get<boolean>(v1Keys.minimizeOnCopyToClipboard)) ??
|
||||
defaultAccount.settings.minimizeOnCopyToClipboard,
|
||||
neverDomains:
|
||||
(await this.get<any>(v1Keys.neverDomains)) ?? defaultAccount.settings.neverDomains,
|
||||
passwordGenerationOptions:
|
||||
(await this.get<any>(v1Keys.passwordGenerationOptions)) ??
|
||||
defaultAccount.settings.passwordGenerationOptions,
|
||||
pinProtected: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<string>(v1Keys.pinProtected),
|
||||
},
|
||||
protectedPin: await this.get<string>(v1Keys.protectedPin),
|
||||
settings: userId == null ? null : await this.get<any>(v1KeyPrefixes.settings + userId),
|
||||
vaultTimeout:
|
||||
(await this.get<number>(v1Keys.vaultTimeout)) ?? defaultAccount.settings.vaultTimeout,
|
||||
vaultTimeoutAction:
|
||||
(await this.get<string>(v1Keys.vaultTimeoutAction)) ??
|
||||
defaultAccount.settings.vaultTimeoutAction,
|
||||
};
|
||||
|
||||
// (userId == null) = no logged in user (so no known userId) and we need to temporarily store account specific settings in state to migrate on first auth
|
||||
// (userId != null) = we have a currently authed user (so known userId) with encrypted data and other key settings we can move, no need to temporarily store account settings
|
||||
if (userId == null) {
|
||||
await this.set(keys.tempAccountSettings, accountSettings);
|
||||
await this.set(keys.global, globals);
|
||||
await this.set(keys.authenticatedAccounts, []);
|
||||
await this.set(keys.activeUserId, null);
|
||||
await clearV1Keys();
|
||||
return;
|
||||
}
|
||||
|
||||
globals.twoFactorToken = await this.get<string>(v1KeyPrefixes.twoFactorToken + userId);
|
||||
await this.set(keys.global, globals);
|
||||
await this.set(userId, {
|
||||
data: {
|
||||
addEditCipherInfo: null,
|
||||
ciphers: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: CipherData }>(v1KeyPrefixes.ciphers + userId),
|
||||
},
|
||||
collapsedGroupings: null,
|
||||
collections: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: CollectionData }>(
|
||||
v1KeyPrefixes.collections + userId
|
||||
),
|
||||
},
|
||||
eventCollection: await this.get<EventData[]>(v1Keys.eventCollection),
|
||||
folders: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: FolderData }>(v1KeyPrefixes.folders + userId),
|
||||
},
|
||||
localData: null,
|
||||
organizations: await this.get<{ [id: string]: OrganizationData }>(
|
||||
v1KeyPrefixes.organizations + userId
|
||||
),
|
||||
passwordGenerationHistory: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<GeneratedPasswordHistory[]>(v1Keys.history),
|
||||
},
|
||||
policies: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: PolicyData }>(v1KeyPrefixes.policies + userId),
|
||||
},
|
||||
providers: await this.get<{ [id: string]: ProviderData }>(v1KeyPrefixes.providers + userId),
|
||||
sends: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<{ [id: string]: SendData }>(v1KeyPrefixes.sends + userId),
|
||||
},
|
||||
},
|
||||
keys: {
|
||||
apiKeyClientSecret: await this.get<string>(v1Keys.clientSecret),
|
||||
cryptoMasterKey: null,
|
||||
cryptoMasterKeyAuto: null,
|
||||
cryptoMasterKeyB64: null,
|
||||
cryptoMasterKeyBiometric: null,
|
||||
cryptoSymmetricKey: {
|
||||
encrypted: await this.get<string>(v1Keys.encKey),
|
||||
decrypted: null,
|
||||
},
|
||||
legacyEtmKey: null,
|
||||
organizationKeys: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<any>(v1Keys.encOrgKeys),
|
||||
},
|
||||
privateKey: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<string>(v1Keys.encPrivate),
|
||||
},
|
||||
providerKeys: {
|
||||
decrypted: null,
|
||||
encrypted: await this.get<any>(v1Keys.encProviderKeys),
|
||||
},
|
||||
publicKey: null,
|
||||
},
|
||||
profile: {
|
||||
apiKeyClientId: await this.get<string>(v1Keys.clientId),
|
||||
authenticationStatus: null,
|
||||
convertAccountToKeyConnector: await this.get<boolean>(v1Keys.convertAccountToKeyConnector),
|
||||
email: await this.get<string>(v1Keys.userEmail),
|
||||
emailVerified: await this.get<boolean>(v1Keys.emailVerified),
|
||||
entityId: null,
|
||||
entityType: null,
|
||||
everBeenUnlocked: null,
|
||||
forcePasswordReset: null,
|
||||
hasPremiumPersonally: null,
|
||||
kdfIterations: await this.get<number>(v1Keys.kdfIterations),
|
||||
kdfType: await this.get<KdfType>(v1Keys.kdf),
|
||||
keyHash: await this.get<string>(v1Keys.keyHash),
|
||||
lastSync: null,
|
||||
userId: userId,
|
||||
usesKeyConnector: null,
|
||||
},
|
||||
settings: accountSettings,
|
||||
tokens: {
|
||||
accessToken: await this.get<string>(v1Keys.accessToken),
|
||||
decodedToken: null,
|
||||
refreshToken: await this.get<string>(v1Keys.refreshToken),
|
||||
securityStamp: null,
|
||||
},
|
||||
});
|
||||
|
||||
await this.set(keys.authenticatedAccounts, [userId]);
|
||||
await this.set(keys.activeUserId, userId);
|
||||
|
||||
const accountActivity: { [userId: string]: number } = {
|
||||
[userId]: await this.get<number>(v1Keys.lastActive),
|
||||
};
|
||||
accountActivity[userId] = await this.get<number>(v1Keys.lastActive);
|
||||
await this.set(keys.accountActivity, accountActivity);
|
||||
|
||||
await clearV1Keys(userId);
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}${partialKeys.biometricKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }),
|
||||
{ keySuffix: "biometric" }
|
||||
);
|
||||
await this.secureStorageService.remove(v1Keys.key, { keySuffix: "biometric" });
|
||||
}
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}${partialKeys.autoKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }),
|
||||
{ keySuffix: "auto" }
|
||||
);
|
||||
await this.secureStorageService.remove(v1Keys.key, { keySuffix: "auto" });
|
||||
}
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key)) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}${partialKeys.masterKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key)
|
||||
);
|
||||
await this.secureStorageService.remove(v1Keys.key);
|
||||
}
|
||||
}
|
||||
|
||||
protected async migrateStateFrom2To3(): Promise<void> {
|
||||
const authenticatedUserIds = await this.get<string[]>(keys.authenticatedAccounts);
|
||||
await Promise.all(
|
||||
authenticatedUserIds.map(async (userId) => {
|
||||
const account = await this.get<TAccount>(userId);
|
||||
if (
|
||||
account?.profile?.hasPremiumPersonally === null &&
|
||||
account.tokens?.accessToken != null
|
||||
) {
|
||||
const decodedToken = await TokenService.decodeToken(account.tokens.accessToken);
|
||||
account.profile.hasPremiumPersonally = decodedToken.premium;
|
||||
await this.set(userId, account);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Three;
|
||||
await this.set(keys.global, globals);
|
||||
}
|
||||
|
||||
protected async migrateStateFrom3To4(): Promise<void> {
|
||||
const authenticatedUserIds = await this.get<string[]>(keys.authenticatedAccounts);
|
||||
await Promise.all(
|
||||
authenticatedUserIds.map(async (userId) => {
|
||||
const account = await this.get<TAccount>(userId);
|
||||
if (account?.profile?.everBeenUnlocked != null) {
|
||||
delete account.profile.everBeenUnlocked;
|
||||
return this.set(userId, account);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const globals = await this.getGlobals();
|
||||
globals.stateVersion = StateVersion.Four;
|
||||
await this.set(keys.global, globals);
|
||||
}
|
||||
|
||||
protected get options(): StorageOptions {
|
||||
return { htmlStorageLocation: HtmlStorageLocation.Local };
|
||||
}
|
||||
|
||||
protected get<T>(key: string): Promise<T> {
|
||||
return this.storageService.get<T>(key, this.options);
|
||||
}
|
||||
|
||||
protected set(key: string, value: any): Promise<any> {
|
||||
if (value == null) {
|
||||
return this.storageService.remove(key, this.options);
|
||||
}
|
||||
return this.storageService.save(key, value, this.options);
|
||||
}
|
||||
|
||||
protected async getGlobals(): Promise<TGlobalState> {
|
||||
return await this.get<TGlobalState>(keys.global);
|
||||
}
|
||||
|
||||
protected async getCurrentStateVersion(): Promise<StateVersion> {
|
||||
return (await this.getGlobals())?.stateVersion ?? StateVersion.One;
|
||||
}
|
||||
}
|
||||
397
libs/common/src/services/sync.service.ts
Normal file
397
libs/common/src/services/sync.service.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
const response = await this.apiService.getSync();
|
||||
|
||||
await this.syncProfile(response.profile);
|
||||
await this.syncFolders(response.folders);
|
||||
await this.syncCollections(response.collections);
|
||||
await this.syncCiphers(response.ciphers);
|
||||
await this.syncSends(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) {
|
||||
await this.folderService.upsert(new FolderData(remoteFolder));
|
||||
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) {
|
||||
await this.cipherService.upsert(new CipherData(remoteCipher));
|
||||
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) {
|
||||
await this.sendService.upsert(new SendData(remoteSend));
|
||||
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.setCanAccessPremium(response.premium);
|
||||
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(response: FolderResponse[]) {
|
||||
const folders: { [id: string]: FolderData } = {};
|
||||
response.forEach((f) => {
|
||||
folders[f.id] = new FolderData(f);
|
||||
});
|
||||
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(response: CipherResponse[]) {
|
||||
const ciphers: { [id: string]: CipherData } = {};
|
||||
response.forEach((c) => {
|
||||
ciphers[c.id] = new CipherData(c);
|
||||
});
|
||||
return await this.cipherService.replace(ciphers);
|
||||
}
|
||||
|
||||
private async syncSends(response: SendResponse[]) {
|
||||
const sends: { [id: string]: SendData } = {};
|
||||
response.forEach((s) => {
|
||||
sends[s.id] = new SendData(s);
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
90
libs/common/src/services/system.service.ts
Normal file
90
libs/common/src/services/system.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
195
libs/common/src/services/token.service.ts
Normal file
195
libs/common/src/services/token.service.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export class TokenService implements TokenServiceAbstraction {
|
||||
static decodeToken(token: string): Promise<any> {
|
||||
if (token == null) {
|
||||
throw new Error("Token not provided.");
|
||||
}
|
||||
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("JWT must have 3 parts");
|
||||
}
|
||||
|
||||
const decoded = Utils.fromUrlB64ToUtf8(parts[1]);
|
||||
if (decoded == null) {
|
||||
throw new Error("Cannot decode the token");
|
||||
}
|
||||
|
||||
const decodedToken = JSON.parse(decoded);
|
||||
return decodedToken;
|
||||
}
|
||||
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
async setTokens(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
clientIdClientSecret: [string, string]
|
||||
): Promise<any> {
|
||||
await this.setToken(accessToken);
|
||||
await this.setRefreshToken(refreshToken);
|
||||
if (clientIdClientSecret != null) {
|
||||
await this.setClientId(clientIdClientSecret[0]);
|
||||
await this.setClientSecret(clientIdClientSecret[1]);
|
||||
}
|
||||
}
|
||||
|
||||
async setClientId(clientId: string): Promise<any> {
|
||||
return await this.stateService.setApiKeyClientId(clientId);
|
||||
}
|
||||
|
||||
async getClientId(): Promise<string> {
|
||||
return await this.stateService.getApiKeyClientId();
|
||||
}
|
||||
|
||||
async setClientSecret(clientSecret: string): Promise<any> {
|
||||
return await this.stateService.setApiKeyClientSecret(clientSecret);
|
||||
}
|
||||
|
||||
async getClientSecret(): Promise<string> {
|
||||
return await this.stateService.getApiKeyClientSecret();
|
||||
}
|
||||
|
||||
async setToken(token: string): Promise<void> {
|
||||
await this.stateService.setAccessToken(token);
|
||||
}
|
||||
|
||||
async getToken(): Promise<string> {
|
||||
return await this.stateService.getAccessToken();
|
||||
}
|
||||
|
||||
async setRefreshToken(refreshToken: string): Promise<any> {
|
||||
return await this.stateService.setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
async getRefreshToken(): Promise<string> {
|
||||
return await this.stateService.getRefreshToken();
|
||||
}
|
||||
|
||||
async setTwoFactorToken(tokenResponse: IdentityTokenResponse): Promise<any> {
|
||||
return await this.stateService.setTwoFactorToken(tokenResponse.twoFactorToken);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(): Promise<string> {
|
||||
return await this.stateService.getTwoFactorToken();
|
||||
}
|
||||
|
||||
async clearTwoFactorToken(): Promise<any> {
|
||||
return await this.stateService.setTwoFactorToken(null);
|
||||
}
|
||||
|
||||
async clearToken(userId?: string): Promise<any> {
|
||||
await this.stateService.setAccessToken(null, { userId: userId });
|
||||
await this.stateService.setRefreshToken(null, { userId: userId });
|
||||
await this.stateService.setApiKeyClientId(null, { userId: userId });
|
||||
await this.stateService.setApiKeyClientSecret(null, { userId: userId });
|
||||
}
|
||||
|
||||
// jwthelper methods
|
||||
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
|
||||
|
||||
async decodeToken(token?: string): Promise<any> {
|
||||
const storedToken = await this.stateService.getDecodedToken();
|
||||
if (token === null && storedToken != null) {
|
||||
return storedToken;
|
||||
}
|
||||
|
||||
token = token ?? (await this.stateService.getAccessToken());
|
||||
|
||||
if (token == null) {
|
||||
throw new Error("Token not found.");
|
||||
}
|
||||
|
||||
return TokenService.decodeToken(token);
|
||||
}
|
||||
|
||||
async getTokenExpirationDate(): Promise<Date> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.exp === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const d = new Date(0); // The 0 here is the key, which sets the date to the epoch
|
||||
d.setUTCSeconds(decoded.exp);
|
||||
return d;
|
||||
}
|
||||
|
||||
async tokenSecondsRemaining(offsetSeconds = 0): Promise<number> {
|
||||
const d = await this.getTokenExpirationDate();
|
||||
if (d == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const msRemaining = d.valueOf() - (new Date().valueOf() + offsetSeconds * 1000);
|
||||
return Math.round(msRemaining / 1000);
|
||||
}
|
||||
|
||||
async tokenNeedsRefresh(minutes = 5): Promise<boolean> {
|
||||
const sRemaining = await this.tokenSecondsRemaining();
|
||||
return sRemaining < 60 * minutes;
|
||||
}
|
||||
|
||||
async getUserId(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.sub === "undefined") {
|
||||
throw new Error("No user id found");
|
||||
}
|
||||
|
||||
return decoded.sub as string;
|
||||
}
|
||||
|
||||
async getEmail(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.email === "undefined") {
|
||||
throw new Error("No email found");
|
||||
}
|
||||
|
||||
return decoded.email as string;
|
||||
}
|
||||
|
||||
async getEmailVerified(): Promise<boolean> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.email_verified === "undefined") {
|
||||
throw new Error("No email verification found");
|
||||
}
|
||||
|
||||
return decoded.email_verified as boolean;
|
||||
}
|
||||
|
||||
async getName(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.name === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decoded.name as string;
|
||||
}
|
||||
|
||||
async getPremium(): Promise<boolean> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.premium === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return decoded.premium as boolean;
|
||||
}
|
||||
|
||||
async getIssuer(): Promise<string> {
|
||||
const decoded = await this.decodeToken();
|
||||
if (typeof decoded.iss === "undefined") {
|
||||
throw new Error("No issuer found");
|
||||
}
|
||||
|
||||
return decoded.iss as string;
|
||||
}
|
||||
|
||||
async getIsExternal(): Promise<boolean> {
|
||||
const decoded = await this.decodeToken();
|
||||
|
||||
return Array.isArray(decoded.amr) && decoded.amr.includes("external");
|
||||
}
|
||||
}
|
||||
174
libs/common/src/services/totp.service.ts
Normal file
174
libs/common/src/services/totp.service.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
186
libs/common/src/services/twoFactor.service.ts
Normal file
186
libs/common/src/services/twoFactor.service.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||
import {
|
||||
TwoFactorProviderDetails,
|
||||
TwoFactorService as TwoFactorServiceAbstraction,
|
||||
} from "../abstractions/twoFactor.service";
|
||||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
|
||||
|
||||
export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactorProviderDetails>> =
|
||||
{
|
||||
[TwoFactorProviderType.Authenticator]: {
|
||||
type: TwoFactorProviderType.Authenticator,
|
||||
name: null as string,
|
||||
description: null as string,
|
||||
priority: 1,
|
||||
sort: 1,
|
||||
premium: false,
|
||||
},
|
||||
[TwoFactorProviderType.Yubikey]: {
|
||||
type: TwoFactorProviderType.Yubikey,
|
||||
name: null as string,
|
||||
description: null as string,
|
||||
priority: 3,
|
||||
sort: 2,
|
||||
premium: true,
|
||||
},
|
||||
[TwoFactorProviderType.Duo]: {
|
||||
type: TwoFactorProviderType.Duo,
|
||||
name: "Duo",
|
||||
description: null as string,
|
||||
priority: 2,
|
||||
sort: 3,
|
||||
premium: true,
|
||||
},
|
||||
[TwoFactorProviderType.OrganizationDuo]: {
|
||||
type: TwoFactorProviderType.OrganizationDuo,
|
||||
name: "Duo (Organization)",
|
||||
description: null as string,
|
||||
priority: 10,
|
||||
sort: 4,
|
||||
premium: false,
|
||||
},
|
||||
[TwoFactorProviderType.Email]: {
|
||||
type: TwoFactorProviderType.Email,
|
||||
name: null as string,
|
||||
description: null as string,
|
||||
priority: 0,
|
||||
sort: 6,
|
||||
premium: false,
|
||||
},
|
||||
[TwoFactorProviderType.WebAuthn]: {
|
||||
type: TwoFactorProviderType.WebAuthn,
|
||||
name: null as string,
|
||||
description: null as string,
|
||||
priority: 4,
|
||||
sort: 5,
|
||||
premium: true,
|
||||
},
|
||||
};
|
||||
|
||||
export class TwoFactorService implements TwoFactorServiceAbstraction {
|
||||
private twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
|
||||
private selectedTwoFactorProviderType: TwoFactorProviderType = null;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService
|
||||
) {}
|
||||
|
||||
init() {
|
||||
TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t("emailTitle");
|
||||
TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t("emailDesc");
|
||||
|
||||
TwoFactorProviders[TwoFactorProviderType.Authenticator].name =
|
||||
this.i18nService.t("authenticatorAppTitle");
|
||||
TwoFactorProviders[TwoFactorProviderType.Authenticator].description =
|
||||
this.i18nService.t("authenticatorAppDesc");
|
||||
|
||||
TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t("duoDesc");
|
||||
|
||||
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name =
|
||||
"Duo (" + this.i18nService.t("organization") + ")";
|
||||
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description =
|
||||
this.i18nService.t("duoOrganizationDesc");
|
||||
|
||||
TwoFactorProviders[TwoFactorProviderType.WebAuthn].name = this.i18nService.t("webAuthnTitle");
|
||||
TwoFactorProviders[TwoFactorProviderType.WebAuthn].description =
|
||||
this.i18nService.t("webAuthnDesc");
|
||||
|
||||
TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t("yubiKeyTitle");
|
||||
TwoFactorProviders[TwoFactorProviderType.Yubikey].description =
|
||||
this.i18nService.t("yubiKeyDesc");
|
||||
}
|
||||
|
||||
getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
|
||||
const providers: any[] = [];
|
||||
if (this.twoFactorProvidersData == null) {
|
||||
return providers;
|
||||
}
|
||||
|
||||
if (
|
||||
this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) &&
|
||||
this.platformUtilsService.supportsDuo()
|
||||
) {
|
||||
providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
|
||||
}
|
||||
|
||||
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) {
|
||||
providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
|
||||
}
|
||||
|
||||
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) {
|
||||
providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
|
||||
}
|
||||
|
||||
if (
|
||||
this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) &&
|
||||
this.platformUtilsService.supportsDuo()
|
||||
) {
|
||||
providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
|
||||
}
|
||||
|
||||
if (
|
||||
this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) &&
|
||||
this.platformUtilsService.supportsWebAuthn(win)
|
||||
) {
|
||||
providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
|
||||
}
|
||||
|
||||
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) {
|
||||
providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType {
|
||||
if (this.twoFactorProvidersData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
this.selectedTwoFactorProviderType != null &&
|
||||
this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType)
|
||||
) {
|
||||
return this.selectedTwoFactorProviderType;
|
||||
}
|
||||
|
||||
let providerType: TwoFactorProviderType = null;
|
||||
let providerPriority = -1;
|
||||
this.twoFactorProvidersData.forEach((_value, type) => {
|
||||
const provider = (TwoFactorProviders as any)[type];
|
||||
if (provider != null && provider.priority > providerPriority) {
|
||||
if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
providerType = type;
|
||||
providerPriority = provider.priority;
|
||||
}
|
||||
});
|
||||
|
||||
return providerType;
|
||||
}
|
||||
|
||||
setSelectedProvider(type: TwoFactorProviderType) {
|
||||
this.selectedTwoFactorProviderType = type;
|
||||
}
|
||||
|
||||
clearSelectedProvider() {
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
}
|
||||
|
||||
setProviders(response: IdentityTwoFactorResponse) {
|
||||
this.twoFactorProvidersData = response.twoFactorProviders2;
|
||||
}
|
||||
|
||||
clearProviders() {
|
||||
this.twoFactorProvidersData = null;
|
||||
}
|
||||
|
||||
getProviders() {
|
||||
return this.twoFactorProvidersData;
|
||||
}
|
||||
}
|
||||
88
libs/common/src/services/userVerification.service.ts
Normal file
88
libs/common/src/services/userVerification.service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
277
libs/common/src/services/usernameGeneration.service.ts
Normal file
277
libs/common/src/services/usernameGeneration.service.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
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",
|
||||
forwardedService: "simplelogin",
|
||||
forwardedAnonAddyDomain: "anonaddy.me",
|
||||
};
|
||||
|
||||
export class UsernameGenerationService implements BaseUsernameGenerationService {
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService
|
||||
) {}
|
||||
|
||||
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.generateForwarded(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 generateForwarded(options: any): Promise<string> {
|
||||
const o = Object.assign({}, DefaultOptions, options);
|
||||
|
||||
if (o.forwardedService == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (o.forwardedService === "simplelogin") {
|
||||
if (o.forwardedSimpleLoginApiKey == null || o.forwardedSimpleLoginApiKey === "") {
|
||||
return null;
|
||||
}
|
||||
return this.generateSimpleLoginAlias(o.forwardedSimpleLoginApiKey, o.website);
|
||||
} else if (o.forwardedService === "anonaddy") {
|
||||
if (
|
||||
o.forwardedAnonAddyApiToken == null ||
|
||||
o.forwardedAnonAddyApiToken === "" ||
|
||||
o.forwardedAnonAddyDomain == null ||
|
||||
o.forwardedAnonAddyDomain == ""
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return this.generateAnonAddyAlias(
|
||||
o.forwardedAnonAddyApiToken,
|
||||
o.forwardedAnonAddyDomain,
|
||||
o.website
|
||||
);
|
||||
} else if (o.forwardedService === "firefoxrelay") {
|
||||
if (o.forwardedFirefoxApiToken == null || o.forwardedFirefoxApiToken === "") {
|
||||
return null;
|
||||
}
|
||||
return this.generateFirefoxRelayAlias(o.forwardedFirefoxApiToken, o.website);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async generateSimpleLoginAlias(apiKey: string, website: string): Promise<string> {
|
||||
if (apiKey == null || apiKey === "") {
|
||||
throw "Invalid SimpleLogin API key.";
|
||||
}
|
||||
const requestInit: RequestInit = {
|
||||
redirect: "manual",
|
||||
cache: "no-store",
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
Authentication: apiKey,
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
};
|
||||
let url = "https://app.simplelogin.io/api/alias/random/new";
|
||||
if (website != null) {
|
||||
url += "?hostname=" + website;
|
||||
}
|
||||
requestInit.body = JSON.stringify({
|
||||
note: (website != null ? "Website: " + website + ". " : "") + "Generated by Bitwarden.",
|
||||
});
|
||||
const request = new Request(url, requestInit);
|
||||
const response = await this.apiService.nativeFetch(request);
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
const json = await response.json();
|
||||
return json.alias;
|
||||
}
|
||||
if (response.status === 401) {
|
||||
throw "Invalid SimpleLogin API key.";
|
||||
}
|
||||
try {
|
||||
const json = await response.json();
|
||||
if (json?.error != null) {
|
||||
throw "SimpleLogin error:" + json.error;
|
||||
}
|
||||
} catch {
|
||||
// Do nothing...
|
||||
}
|
||||
throw "Unknown SimpleLogin error occurred.";
|
||||
}
|
||||
|
||||
private async generateAnonAddyAlias(
|
||||
apiToken: string,
|
||||
domain: string,
|
||||
websiteNote: string
|
||||
): Promise<string> {
|
||||
if (apiToken == null || apiToken === "") {
|
||||
throw "Invalid AnonAddy API token.";
|
||||
}
|
||||
const requestInit: RequestInit = {
|
||||
redirect: "manual",
|
||||
cache: "no-store",
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
Authorization: "Bearer " + apiToken,
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
};
|
||||
const url = "https://app.anonaddy.com/api/v1/aliases";
|
||||
requestInit.body = JSON.stringify({
|
||||
domain: domain,
|
||||
description:
|
||||
(websiteNote != null ? "Website: " + websiteNote + ". " : "") + "Generated by Bitwarden.",
|
||||
});
|
||||
const request = new Request(url, requestInit);
|
||||
const response = await this.apiService.nativeFetch(request);
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
const json = await response.json();
|
||||
return json?.data?.email;
|
||||
}
|
||||
if (response.status === 401) {
|
||||
throw "Invalid AnonAddy API token.";
|
||||
}
|
||||
throw "Unknown AnonAddy error occurred.";
|
||||
}
|
||||
|
||||
private async generateFirefoxRelayAlias(apiToken: string, website: string): Promise<string> {
|
||||
if (apiToken == null || apiToken === "") {
|
||||
throw "Invalid Firefox Relay API token.";
|
||||
}
|
||||
const requestInit: RequestInit = {
|
||||
redirect: "manual",
|
||||
cache: "no-store",
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
Authorization: "Token " + apiToken,
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
};
|
||||
const url = "https://relay.firefox.com/api/v1/relayaddresses/";
|
||||
requestInit.body = JSON.stringify({
|
||||
enabled: true,
|
||||
generated_for: website,
|
||||
description: (website != null ? website + " - " : "") + "Generated by Bitwarden.",
|
||||
});
|
||||
const request = new Request(url, requestInit);
|
||||
const response = await this.apiService.nativeFetch(request);
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
const json = await response.json();
|
||||
return json?.full_address;
|
||||
}
|
||||
if (response.status === 401) {
|
||||
throw "Invalid Firefox Relay API token.";
|
||||
}
|
||||
throw "Unknown Firefox Relay error occurred.";
|
||||
}
|
||||
}
|
||||
206
libs/common/src/services/vaultTimeout.service.ts
Normal file
206
libs/common/src/services/vaultTimeout.service.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { AuthService } from "../abstractions/auth.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 { 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 { AuthenticationStatus } from "../enums/authenticationStatus";
|
||||
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 authService: AuthService,
|
||||
private lockedCallback: (userId?: string) => Promise<void> = null,
|
||||
private loggedOutCallback: (expired: boolean, 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
|
||||
}
|
||||
|
||||
async checkVaultTimeout(): Promise<void> {
|
||||
if (await this.platformUtilsService.isViewOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const userId in this.stateService.accounts.getValue()) {
|
||||
if (userId != null && (await this.shouldLock(userId))) {
|
||||
await this.executeTimeoutAction(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(false, 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 shouldLock(userId: string): Promise<boolean> {
|
||||
const authStatus = await this.authService.getAuthStatus(userId);
|
||||
if (
|
||||
authStatus === AuthenticationStatus.Locked ||
|
||||
authStatus === AuthenticationStatus.LoggedOut
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
356
libs/common/src/services/webCryptoFunction.service.ts
Normal file
356
libs/common/src/services/webCryptoFunction.service.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import * as forge from "node-forge";
|
||||
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { DecryptParameters } from "../models/domain/decryptParameters";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
|
||||
export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
private crypto: Crypto;
|
||||
private subtle: SubtleCrypto;
|
||||
|
||||
constructor(win: Window) {
|
||||
this.crypto = typeof win.crypto !== "undefined" ? win.crypto : null;
|
||||
this.subtle =
|
||||
!!this.crypto && typeof win.crypto.subtle !== "undefined" ? win.crypto.subtle : null;
|
||||
}
|
||||
|
||||
async pbkdf2(
|
||||
password: string | ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
algorithm: "sha256" | "sha512",
|
||||
iterations: number
|
||||
): Promise<ArrayBuffer> {
|
||||
const wcLen = algorithm === "sha256" ? 256 : 512;
|
||||
const passwordBuf = this.toBuf(password);
|
||||
const saltBuf = this.toBuf(salt);
|
||||
|
||||
const pbkdf2Params: Pbkdf2Params = {
|
||||
name: "PBKDF2",
|
||||
salt: saltBuf,
|
||||
iterations: iterations,
|
||||
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
};
|
||||
|
||||
const impKey = await this.subtle.importKey(
|
||||
"raw",
|
||||
passwordBuf,
|
||||
{ name: "PBKDF2" } as any,
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen);
|
||||
}
|
||||
|
||||
async hkdf(
|
||||
ikm: ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
info: string | ArrayBuffer,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
const saltBuf = this.toBuf(salt);
|
||||
const infoBuf = this.toBuf(info);
|
||||
|
||||
const hkdfParams: HkdfParams = {
|
||||
name: "HKDF",
|
||||
salt: saltBuf,
|
||||
info: infoBuf,
|
||||
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
};
|
||||
|
||||
const impKey = await this.subtle.importKey("raw", ikm, { name: "HKDF" } as any, false, [
|
||||
"deriveBits",
|
||||
]);
|
||||
return await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8);
|
||||
}
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
async hkdfExpand(
|
||||
prk: ArrayBuffer,
|
||||
info: string | ArrayBuffer,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
const hashLen = algorithm === "sha256" ? 32 : 64;
|
||||
if (outputByteSize > 255 * hashLen) {
|
||||
throw new Error("outputByteSize is too large.");
|
||||
}
|
||||
const prkArr = new Uint8Array(prk);
|
||||
if (prkArr.length < hashLen) {
|
||||
throw new Error("prk is too small.");
|
||||
}
|
||||
const infoBuf = this.toBuf(info);
|
||||
const infoArr = new Uint8Array(infoBuf);
|
||||
let runningOkmLength = 0;
|
||||
let previousT = new Uint8Array(0);
|
||||
const n = Math.ceil(outputByteSize / hashLen);
|
||||
const okm = new Uint8Array(n * hashLen);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = new Uint8Array(previousT.length + infoArr.length + 1);
|
||||
t.set(previousT);
|
||||
t.set(infoArr, previousT.length);
|
||||
t.set([i + 1], t.length - 1);
|
||||
previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm));
|
||||
okm.set(previousT, runningOkmLength);
|
||||
runningOkmLength += previousT.length;
|
||||
if (runningOkmLength >= outputByteSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return okm.slice(0, outputByteSize).buffer;
|
||||
}
|
||||
|
||||
async hash(
|
||||
value: string | ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256" | "sha512" | "md5"
|
||||
): Promise<ArrayBuffer> {
|
||||
if (algorithm === "md5") {
|
||||
const md = algorithm === "md5" ? forge.md.md5.create() : forge.md.sha1.create();
|
||||
const valueBytes = this.toByteString(value);
|
||||
md.update(valueBytes, "raw");
|
||||
return Utils.fromByteStringToArray(md.digest().data).buffer;
|
||||
}
|
||||
|
||||
const valueBuf = this.toBuf(value);
|
||||
return await this.subtle.digest({ name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf);
|
||||
}
|
||||
|
||||
async hmac(
|
||||
value: ArrayBuffer,
|
||||
key: ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
const signingAlgorithm = {
|
||||
name: "HMAC",
|
||||
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
};
|
||||
|
||||
const impKey = await this.subtle.importKey("raw", key, signingAlgorithm, false, ["sign"]);
|
||||
return await this.subtle.sign(signingAlgorithm, impKey, value);
|
||||
}
|
||||
|
||||
// Safely compare two values in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
||||
async compare(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
|
||||
const macKey = await this.randomBytes(32);
|
||||
const signingAlgorithm = {
|
||||
name: "HMAC",
|
||||
hash: { name: "SHA-256" },
|
||||
};
|
||||
const impKey = await this.subtle.importKey("raw", macKey, signingAlgorithm, false, ["sign"]);
|
||||
const mac1 = await this.subtle.sign(signingAlgorithm, impKey, a);
|
||||
const mac2 = await this.subtle.sign(signingAlgorithm, impKey, b);
|
||||
|
||||
if (mac1.byteLength !== mac2.byteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const arr1 = new Uint8Array(mac1);
|
||||
const arr2 = new Uint8Array(mac2);
|
||||
for (let i = 0; i < arr2.length; i++) {
|
||||
if (arr1[i] !== arr2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
hmacFast(value: string, key: string, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
|
||||
const hmac = forge.hmac.create();
|
||||
hmac.start(algorithm, key);
|
||||
hmac.update(value);
|
||||
const bytes = hmac.digest().getBytes();
|
||||
return Promise.resolve(bytes);
|
||||
}
|
||||
|
||||
async compareFast(a: string, b: string): Promise<boolean> {
|
||||
const rand = await this.randomBytes(32);
|
||||
const bytes = new Uint32Array(rand);
|
||||
const buffer = forge.util.createBuffer();
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
buffer.putInt32(bytes[i]);
|
||||
}
|
||||
const macKey = buffer.getBytes();
|
||||
|
||||
const hmac = forge.hmac.create();
|
||||
hmac.start("sha256", macKey);
|
||||
hmac.update(a);
|
||||
const mac1 = hmac.digest().getBytes();
|
||||
|
||||
hmac.start(null, null);
|
||||
hmac.update(b);
|
||||
const mac2 = hmac.digest().getBytes();
|
||||
|
||||
const equals = mac1 === mac2;
|
||||
return equals;
|
||||
}
|
||||
|
||||
async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
|
||||
"encrypt",
|
||||
]);
|
||||
return await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data);
|
||||
}
|
||||
|
||||
aesDecryptFastParameters(
|
||||
data: string,
|
||||
iv: string,
|
||||
mac: string,
|
||||
key: SymmetricCryptoKey
|
||||
): DecryptParameters<string> {
|
||||
const p = new DecryptParameters<string>();
|
||||
if (key.meta != null) {
|
||||
p.encKey = key.meta.encKeyByteString;
|
||||
p.macKey = key.meta.macKeyByteString;
|
||||
}
|
||||
|
||||
if (p.encKey == null) {
|
||||
p.encKey = forge.util.decode64(key.encKeyB64);
|
||||
}
|
||||
p.data = forge.util.decode64(data);
|
||||
p.iv = forge.util.decode64(iv);
|
||||
p.macData = p.iv + p.data;
|
||||
if (p.macKey == null && key.macKeyB64 != null) {
|
||||
p.macKey = forge.util.decode64(key.macKeyB64);
|
||||
}
|
||||
if (mac != null) {
|
||||
p.mac = forge.util.decode64(mac);
|
||||
}
|
||||
|
||||
// cache byte string keys for later
|
||||
if (key.meta == null) {
|
||||
key.meta = {};
|
||||
}
|
||||
if (key.meta.encKeyByteString == null) {
|
||||
key.meta.encKeyByteString = p.encKey;
|
||||
}
|
||||
if (p.macKey != null && key.meta.macKeyByteString == null) {
|
||||
key.meta.macKeyByteString = p.macKey;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
aesDecryptFast(parameters: DecryptParameters<string>): Promise<string> {
|
||||
const dataBuffer = forge.util.createBuffer(parameters.data);
|
||||
const decipher = forge.cipher.createDecipher("AES-CBC", parameters.encKey);
|
||||
decipher.start({ iv: parameters.iv });
|
||||
decipher.update(dataBuffer);
|
||||
decipher.finish();
|
||||
const val = decipher.output.toString();
|
||||
return Promise.resolve(val);
|
||||
}
|
||||
|
||||
async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
|
||||
"decrypt",
|
||||
]);
|
||||
return await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
|
||||
}
|
||||
|
||||
async rsaEncrypt(
|
||||
data: ArrayBuffer,
|
||||
publicKey: ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256"
|
||||
): Promise<ArrayBuffer> {
|
||||
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
|
||||
// We cannot use the proper types here.
|
||||
const rsaParams = {
|
||||
name: "RSA-OAEP",
|
||||
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
};
|
||||
const impKey = await this.subtle.importKey("spki", publicKey, rsaParams, false, ["encrypt"]);
|
||||
return await this.subtle.encrypt(rsaParams, impKey, data);
|
||||
}
|
||||
|
||||
async rsaDecrypt(
|
||||
data: ArrayBuffer,
|
||||
privateKey: ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256"
|
||||
): Promise<ArrayBuffer> {
|
||||
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
|
||||
// We cannot use the proper types here.
|
||||
const rsaParams = {
|
||||
name: "RSA-OAEP",
|
||||
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
|
||||
};
|
||||
const impKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, false, ["decrypt"]);
|
||||
return await this.subtle.decrypt(rsaParams, impKey, data);
|
||||
}
|
||||
|
||||
async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const rsaParams = {
|
||||
name: "RSA-OAEP",
|
||||
// Have to specify some algorithm
|
||||
hash: { name: this.toWebCryptoAlgorithm("sha1") },
|
||||
};
|
||||
const impPrivateKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, true, [
|
||||
"decrypt",
|
||||
]);
|
||||
const jwkPrivateKey = await this.subtle.exportKey("jwk", impPrivateKey);
|
||||
const jwkPublicKeyParams = {
|
||||
kty: "RSA",
|
||||
e: jwkPrivateKey.e,
|
||||
n: jwkPrivateKey.n,
|
||||
alg: "RSA-OAEP",
|
||||
ext: true,
|
||||
};
|
||||
const impPublicKey = await this.subtle.importKey("jwk", jwkPublicKeyParams, rsaParams, true, [
|
||||
"encrypt",
|
||||
]);
|
||||
return await this.subtle.exportKey("spki", impPublicKey);
|
||||
}
|
||||
|
||||
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> {
|
||||
const rsaParams = {
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: length,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
|
||||
// Have to specify some algorithm
|
||||
hash: { name: this.toWebCryptoAlgorithm("sha1") },
|
||||
};
|
||||
const keyPair = (await this.subtle.generateKey(rsaParams, true, [
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
])) as CryptoKeyPair;
|
||||
const publicKey = await this.subtle.exportKey("spki", keyPair.publicKey);
|
||||
const privateKey = await this.subtle.exportKey("pkcs8", keyPair.privateKey);
|
||||
return [publicKey, privateKey];
|
||||
}
|
||||
|
||||
randomBytes(length: number): Promise<ArrayBuffer> {
|
||||
const arr = new Uint8Array(length);
|
||||
this.crypto.getRandomValues(arr);
|
||||
return Promise.resolve(arr.buffer);
|
||||
}
|
||||
|
||||
private toBuf(value: string | ArrayBuffer): ArrayBuffer {
|
||||
let buf: ArrayBuffer;
|
||||
if (typeof value === "string") {
|
||||
buf = Utils.fromUtf8ToArray(value).buffer;
|
||||
} else {
|
||||
buf = value;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private toByteString(value: string | ArrayBuffer): string {
|
||||
let bytes: string;
|
||||
if (typeof value === "string") {
|
||||
bytes = forge.util.encodeUtf8(value);
|
||||
} else {
|
||||
bytes = Utils.fromBufferToByteString(value);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private toWebCryptoAlgorithm(algorithm: "sha1" | "sha256" | "sha512" | "md5"): string {
|
||||
if (algorithm === "md5") {
|
||||
throw new Error("MD5 is not supported in WebCrypto.");
|
||||
}
|
||||
return algorithm === "sha1" ? "SHA-1" : algorithm === "sha256" ? "SHA-256" : "SHA-512";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user