1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

Merge branch 'master' into feature/org-admin-refresh

This commit is contained in:
Vincent Salucci
2022-08-10 12:02:52 -05:00
100 changed files with 986 additions and 355 deletions

View File

@@ -1,7 +1,6 @@
import { OrganizationApiKeyType } from "../enums/organizationApiKeyType";
import { OrganizationConnectionType } from "../enums/organizationConnectionType";
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
import { AttachmentRequest } from "../models/request/attachmentRequest";
import { BitPayInvoiceRequest } from "../models/request/bitPayInvoiceRequest";
import { CipherBulkDeleteRequest } from "../models/request/cipherBulkDeleteRequest";
@@ -228,8 +227,6 @@ export abstract class ApiService {
postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
postAccountRequestOTP: () => Promise<void>;
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
postConvertToKeyConnector: () => Promise<void>;
getUserBillingHistory: () => Promise<BillingHistoryResponse>;

View File

@@ -6,6 +6,4 @@ export abstract class LogService {
warning: (message: string) => void;
error: (message: string) => void;
write: (level: LogLevelType, message: string) => void;
time: (label: string) => void;
timeEnd: (label: string) => [number, number];
}

View File

@@ -26,9 +26,8 @@ import { SendView } from "../models/view/sendView";
export abstract class StateService<T extends Account = Account> {
accounts: BehaviorSubject<{ [userId: string]: T }>;
activeAccount: BehaviorSubject<string>;
activeAccountUnlocked: Observable<boolean>;
activeAccount$: Observable<string>;
activeAccountUnlocked$: Observable<boolean>;
addAccount: (account: T) => Promise<void>;
setActiveUser: (userId: string) => Promise<void>;

View File

@@ -0,0 +1,6 @@
import { VerifyOTPRequest } from "@bitwarden/common/models/request/account/verifyOTPRequest";
export abstract class UserVerificationApiServiceAbstraction {
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
postAccountRequestOTP: () => Promise<void>;
}

View File

@@ -1,5 +1,5 @@
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
import { Verification } from "../types/verification";
import { SecretVerificationRequest } from "../../models/request/secretVerificationRequest";
import { Verification } from "../../types/verification";
export abstract class UserVerificationService {
buildRequest: <T extends SecretVerificationRequest>(

View File

@@ -1,17 +1,28 @@
/* eslint-disable no-useless-escape */
import * as tldjs from "tldjs";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
const nodeURL = typeof window === "undefined" ? require("url") : null;
declare global {
/* eslint-disable-next-line no-var */
var bitwardenContainerService: BitwardenContainerService;
}
interface BitwardenContainerService {
getCryptoService: () => CryptoService;
}
export class Utils {
static inited = false;
static isNode = false;
static isBrowser = true;
static isMobileBrowser = false;
static isAppleMobileBrowser = false;
static global: any = null;
static global: typeof global = null;
static tldEndingRegex =
/.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/;
// Transpiled version of /\p{Emoji_Presentation}/gu using https://mothereff.in/regexpu. Used for compatability in older browsers.
@@ -29,16 +40,25 @@ export class Utils {
(process as any).release != null &&
(process as any).release.name === "node";
Utils.isBrowser = typeof window !== "undefined";
Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window);
Utils.isAppleMobileBrowser = Utils.isBrowser && this.isAppleMobile(window);
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
if (Utils.isNode) {
Utils.global = global;
} else if (Utils.isBrowser) {
Utils.global = window;
} else {
// If it's not browser or node then it must be a service worker
Utils.global = self;
}
}
static fromB64ToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "base64"));
} else {
const binaryString = window.atob(str);
const binaryString = Utils.global.atob(str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
@@ -93,7 +113,7 @@ export class Utils {
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
return Utils.global.btoa(binary);
}
}
@@ -157,7 +177,7 @@ export class Utils {
if (Utils.isNode) {
return Buffer.from(utfStr, "utf8").toString("base64");
} else {
return decodeURIComponent(escape(window.btoa(utfStr)));
return decodeURIComponent(escape(Utils.global.btoa(utfStr)));
}
}
@@ -169,7 +189,7 @@ export class Utils {
if (Utils.isNode) {
return Buffer.from(b64Str, "base64").toString("utf8");
} else {
return decodeURIComponent(escape(window.atob(b64Str)));
return decodeURIComponent(escape(Utils.global.atob(b64Str)));
}
}
@@ -384,7 +404,7 @@ export class Utils {
return new nodeURL.URL(uriString);
} else if (typeof URL === "function") {
return new URL(uriString);
} else if (window != null) {
} else if (typeof window !== "undefined") {
const hasProtocol = uriString.indexOf("://") > -1;
if (!hasProtocol && uriString.indexOf(".") > -1) {
uriString = "http://" + uriString;

View File

@@ -48,7 +48,7 @@ export class Attachment extends Domain {
if (this.key != null) {
let cryptoService: CryptoService;
const containerService = (Utils.global as any).bitwardenContainerService;
const containerService = Utils.global.bitwardenContainerService;
if (containerService) {
cryptoService = containerService.getCryptoService();
} else {

View File

@@ -104,7 +104,7 @@ export class EncString implements IEncrypted {
}
let cryptoService: CryptoService;
const containerService = (Utils.global as any).bitwardenContainerService;
const containerService = Utils.global.bitwardenContainerService;
if (containerService) {
cryptoService = containerService.getCryptoService();
} else {

View File

@@ -1,7 +1,7 @@
import { AccountApiService } from "@bitwarden/common/abstractions/account/account-api.service.abstraction";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { AccountService as AccountServiceAbstraction } from "../../abstractions/account/account.service.abstraction";
import { Verification } from "../../types/verification";
@@ -14,7 +14,7 @@ export class AccountService implements AccountServiceAbstraction {
private logService: LogService
) {}
async delete(verification: Verification): Promise<any> {
async delete(verification: Verification): Promise<void> {
try {
const verificationRequest = await this.userVerificationService.buildRequest(verification);
await this.accountApiService.deleteAccount(verificationRequest);

View File

@@ -8,7 +8,6 @@ import { OrganizationApiKeyType } from "../enums/organizationApiKeyType";
import { OrganizationConnectionType } from "../enums/organizationConnectionType";
import { Utils } from "../misc/utils";
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
import { AttachmentRequest } from "../models/request/attachmentRequest";
import { BitPayInvoiceRequest } from "../models/request/bitPayInvoiceRequest";
import { CipherBulkDeleteRequest } from "../models/request/cipherBulkDeleteRequest";
@@ -457,14 +456,6 @@ export class ApiService implements ApiServiceAbstraction {
return this.send("PUT", "/accounts/update-temp-password", request, true, false);
}
postAccountRequestOTP(): Promise<void> {
return this.send("POST", "/accounts/request-otp", null, true, false);
}
postAccountVerifyOTP(request: VerifyOTPRequest): Promise<void> {
return this.send("POST", "/accounts/verify-otp", request, true, false);
}
postConvertToKeyConnector(): Promise<void> {
return this.send("POST", "/accounts/convert-to-key-connector", null, true, false);
}

View File

@@ -341,7 +341,7 @@ export class CipherService implements CipherServiceAbstraction {
throw new Error("No key.");
}
const promises: any[] = [];
const promises: Promise<number>[] = [];
const ciphers = await this.getAll();
ciphers.forEach(async (cipher) => {
promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));

View File

@@ -1,5 +1,3 @@
import * as hrtime from "browser-hrtime";
import { LogService as LogServiceAbstraction } from "../abstractions/log.service";
import { LogLevelType } from "../enums/logLevelType";
@@ -56,17 +54,4 @@ export class ConsoleLogService implements LogServiceAbstraction {
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;
}
}

View File

@@ -22,7 +22,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
private scimUrl: string = null;
constructor(private stateService: StateService) {
this.stateService.activeAccount.subscribe(async () => {
this.stateService.activeAccount$.subscribe(async () => {
await this.setUrlsFromStorage();
});
}

View File

@@ -25,7 +25,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
private cipherService: CipherService,
private stateService: StateService
) {
this.stateService.activeAccountUnlocked.subscribe(async (unlocked) => {
this.stateService.activeAccountUnlocked$.subscribe(async (unlocked) => {
if ((Utils.global as any).bitwardenContainerService == null) {
return;
}

View File

@@ -0,0 +1,24 @@
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { EventType } from "@bitwarden/common/enums/eventType";
/**
* If you want to use this, don't.
* If you think you should use that after the warning, don't.
*/
export default class NoOpEventService implements EventService {
constructor() {
if (chrome.runtime.getManifest().manifest_version !== 3) {
throw new Error("You are not allowed to use this when not in manifest_version 3");
}
}
collect(eventType: EventType, cipherId?: string, uploadImmediately?: boolean) {
return Promise.resolve();
}
uploadEvents(userId?: string) {
return Promise.resolve();
}
clearEvents(userId?: string) {
return Promise.resolve();
}
}

View File

@@ -11,6 +11,8 @@ import { CipherView } from "../models/view/cipherView";
import { SendView } from "../models/view/sendView";
export class SearchService implements SearchServiceAbstraction {
private static registeredPipeline = false;
indexedEntityId?: string = null;
private indexing = false;
private index: lunr.Index = null;
@@ -31,8 +33,13 @@ export class SearchService implements SearchServiceAbstraction {
}
});
//register lunr pipeline function
lunr.Pipeline.registerFunction(this.normalizeAccentsPipelineFunction, "normalizeAccents");
// Currently have to ensure this is only done a single time. Lunr allows you to register a function
// multiple times but they will add a warning message to the console. The way they do that breaks when ran on a service worker.
if (!SearchService.registeredPipeline) {
SearchService.registeredPipeline = true;
//register lunr pipeline function
lunr.Pipeline.registerFunction(this.normalizeAccentsPipelineFunction, "normalizeAccents");
}
}
clearIndex(): void {
@@ -54,7 +61,6 @@ export class SearchService implements SearchServiceAbstraction {
return;
}
this.logService.time("search indexing");
this.indexing = true;
this.indexedEntityId = indexedEntityId;
this.index = null;
@@ -95,7 +101,7 @@ export class SearchService implements SearchServiceAbstraction {
this.indexing = false;
this.logService.timeEnd("search indexing");
this.logService.info("Finished search indexing");
}
async searchCiphers(

View File

@@ -55,8 +55,11 @@ export class StateService<
> implements StateServiceAbstraction<TAccount>
{
accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({});
activeAccount = new BehaviorSubject<string>(null);
activeAccountUnlocked = new BehaviorSubject<boolean>(false);
private activeAccountSubject = new BehaviorSubject<string>(null);
activeAccount$ = this.activeAccountSubject.asObservable();
private activeAccountUnlockedSubject = new BehaviorSubject<boolean>(false);
activeAccountUnlocked$ = this.activeAccountUnlockedSubject.asObservable();
private hasBeenInited = false;
private isRecoveredSession = false;
@@ -73,17 +76,17 @@ export class StateService<
protected useAccountCache: boolean = true
) {
// If the account gets changed, verify the new account is unlocked
this.activeAccount.subscribe(async (userId) => {
if (userId == null && this.activeAccountUnlocked.getValue() == false) {
this.activeAccountSubject.subscribe(async (userId) => {
if (userId == null && this.activeAccountUnlockedSubject.getValue() == false) {
return;
} else if (userId == null) {
this.activeAccountUnlocked.next(false);
this.activeAccountUnlockedSubject.next(false);
}
// FIXME: This should be refactored into AuthService or a similar service,
// as checking for the existance of the crypto key is a low level
// implementation detail.
this.activeAccountUnlocked.next((await this.getCryptoMasterKey()) != null);
this.activeAccountUnlockedSubject.next((await this.getCryptoMasterKey()) != null);
});
}
@@ -125,7 +128,7 @@ export class StateService<
state.activeUserId = storedActiveUser;
}
await this.pushAccounts();
this.activeAccount.next(state.activeUserId);
this.activeAccountSubject.next(state.activeUserId);
return state;
});
@@ -154,7 +157,7 @@ export class StateService<
await this.scaffoldNewAccountStorage(account);
await this.setLastActive(new Date().getTime(), { userId: account.profile.userId });
await this.setActiveUser(account.profile.userId);
this.activeAccount.next(account.profile.userId);
this.activeAccountSubject.next(account.profile.userId);
}
async setActiveUser(userId: string): Promise<void> {
@@ -162,7 +165,7 @@ export class StateService<
await this.updateState(async (state) => {
state.activeUserId = userId;
await this.storageService.save(keys.activeUserId, userId);
this.activeAccount.next(state.activeUserId);
this.activeAccountSubject.next(state.activeUserId);
return state;
});
@@ -497,12 +500,12 @@ export class StateService<
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
if (options.userId == this.activeAccount.getValue()) {
if (options.userId == this.activeAccountSubject.getValue()) {
const nextValue = value != null;
// Avoid emitting if we are already unlocked
if (this.activeAccountUnlocked.getValue() != nextValue) {
this.activeAccountUnlocked.next(nextValue);
if (this.activeAccountUnlockedSubject.getValue() != nextValue) {
this.activeAccountUnlockedSubject.next(nextValue);
}
}
}
@@ -607,7 +610,7 @@ export class StateService<
);
}
@withPrototypeForArrayMembers(CipherView)
@withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))

View File

@@ -0,0 +1,14 @@
import { ApiService } from "../../abstractions/api.service";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/userVerification/userVerification-api.service.abstraction";
import { VerifyOTPRequest } from "../../models/request/account/verifyOTPRequest";
export class UserVerificationApiService implements UserVerificationApiServiceAbstraction {
constructor(private apiService: ApiService) {}
postAccountVerifyOTP(request: VerifyOTPRequest): Promise<void> {
return this.apiService.send("POST", "/accounts/verify-otp", request, true, false);
}
async postAccountRequestOTP(): Promise<void> {
return this.apiService.send("POST", "/accounts/request-otp", null, true, false);
}
}

View File

@@ -1,11 +1,11 @@
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";
import { CryptoService } from "../../abstractions/crypto.service";
import { I18nService } from "../../abstractions/i18n.service";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/userVerification/userVerification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/userVerification/userVerification.service.abstraction";
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.
@@ -15,7 +15,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private apiService: ApiService
private userVerificationApiService: UserVerificationApiServiceAbstraction
) {}
/**
@@ -56,7 +56,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
if (verification.type === VerificationType.OTP) {
const request = new VerifyOTPRequest(verification.secret);
try {
await this.apiService.postAccountVerifyOTP(request);
await this.userVerificationApiService.postAccountVerifyOTP(request);
} catch (e) {
throw new Error(this.i18nService.t("invalidVerificationCode"));
}
@@ -73,7 +73,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
}
async requestOTP() {
await this.apiService.postAccountRequestOTP();
await this.userVerificationApiService.postAccountRequestOTP();
}
private validateInput(verification: Verification) {

View File

@@ -9,7 +9,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
private crypto: Crypto;
private subtle: SubtleCrypto;
constructor(win: Window) {
constructor(win: Window | typeof global) {
this.crypto = typeof win.crypto !== "undefined" ? win.crypto : null;
this.subtle =
!!this.crypto && typeof win.crypto.subtle !== "undefined" ? win.crypto.subtle : null;

View File

@@ -0,0 +1,12 @@
import { VerificationType } from "../enums/verificationType";
import { TwoFactorResponse } from "./twoFactorResponse";
export type AuthResponseBase = {
secret: string;
verificationType: VerificationType;
};
export type AuthResponse<T extends TwoFactorResponse> = AuthResponseBase & {
response: T;
};

View File

@@ -0,0 +1,14 @@
import { TwoFactorAuthenticatorResponse } from "../models/response/twoFactorAuthenticatorResponse";
import { TwoFactorDuoResponse } from "../models/response/twoFactorDuoResponse";
import { TwoFactorEmailResponse } from "../models/response/twoFactorEmailResponse";
import { TwoFactorRecoverResponse } from "../models/response/twoFactorRescoverResponse";
import { TwoFactorWebAuthnResponse } from "../models/response/twoFactorWebAuthnResponse";
import { TwoFactorYubiKeyResponse } from "../models/response/twoFactorYubiKeyResponse";
export type TwoFactorResponse =
| TwoFactorRecoverResponse
| TwoFactorDuoResponse
| TwoFactorEmailResponse
| TwoFactorWebAuthnResponse
| TwoFactorAuthenticatorResponse
| TwoFactorYubiKeyResponse;