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

Merge branch 'master' into PM-2135-beeep-refactor-and-refresh-web-user-verification-components

This commit is contained in:
Andreas Coroiu
2023-06-19 14:43:36 +02:00
445 changed files with 8485 additions and 2207 deletions

View File

@@ -1,5 +1,4 @@
import { OrganizationConnectionType } from "../admin-console/enums";
import { CollectionRequest } from "../admin-console/models/request/collection.request";
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/request/organization/organization-sponsorship-redeem.request";
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
@@ -14,10 +13,6 @@ import { ProviderUserConfirmRequest } from "../admin-console/models/request/prov
import { ProviderUserInviteRequest } from "../admin-console/models/request/provider/provider-user-invite.request";
import { ProviderUserUpdateRequest } from "../admin-console/models/request/provider/provider-user-update.request";
import { SelectionReadOnlyRequest } from "../admin-console/models/request/selection-read-only.request";
import {
CollectionAccessDetailsResponse,
CollectionResponse,
} from "../admin-console/models/response/collection.response";
import {
OrganizationConnectionConfigApis,
OrganizationConnectionResponse,
@@ -135,9 +130,14 @@ import { CipherCreateRequest } from "../vault/models/request/cipher-create.reque
import { CipherPartialRequest } from "../vault/models/request/cipher-partial.request";
import { CipherShareRequest } from "../vault/models/request/cipher-share.request";
import { CipherRequest } from "../vault/models/request/cipher.request";
import { CollectionRequest } from "../vault/models/request/collection.request";
import { AttachmentUploadDataResponse } from "../vault/models/response/attachment-upload-data.response";
import { AttachmentResponse } from "../vault/models/response/attachment.response";
import { CipherResponse } from "../vault/models/response/cipher.response";
import {
CollectionAccessDetailsResponse,
CollectionResponse,
} from "../vault/models/response/collection.response";
import { SyncResponse } from "../vault/models/response/sync.response";
/**

View File

@@ -14,6 +14,7 @@ export class OrganizationUserResponse extends BaseResponse {
accessSecretsManager: boolean;
permissions: PermissionsApi;
resetPasswordEnrolled: boolean;
hasMasterPassword: boolean;
collections: SelectionReadOnlyResponse[] = [];
groups: string[] = [];
@@ -28,6 +29,7 @@ export class OrganizationUserResponse extends BaseResponse {
this.accessAll = this.getResponseProperty("AccessAll");
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
const collections = this.getResponseProperty("Collections");
if (collections != null) {

View File

@@ -1,7 +1,6 @@
import { BaseResponse } from "../../../models/response/base.response";
import { CipherResponse } from "../../../vault/models/response/cipher.response";
import { CollectionResponse } from "./collection.response";
import { CollectionResponse } from "../../../vault/models/response/collection.response";
export class OrganizationExportResponse extends BaseResponse {
collections: CollectionResponse[];

View File

@@ -11,7 +11,10 @@ import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { Account, AccountProfile, AccountTokens } from "../../platform/models/domain/account";
import { EncString } from "../../platform/models/domain/enc-string";
import { PasswordGenerationService } from "../../tools/generator/password";
import {
PasswordStrengthService,
PasswordStrengthServiceAbstraction,
} from "../../tools/password-strength";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -85,7 +88,7 @@ describe("LogInStrategy", () => {
let twoFactorService: MockProxy<TwoFactorService>;
let authService: MockProxy<AuthService>;
let policyService: MockProxy<PolicyService>;
let passwordGenerationService: MockProxy<PasswordGenerationService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
@@ -102,7 +105,7 @@ describe("LogInStrategy", () => {
twoFactorService = mock<TwoFactorService>();
authService = mock<AuthService>();
policyService = mock<PolicyService>();
passwordGenerationService = mock<PasswordGenerationService>();
passwordStrengthService = mock<PasswordStrengthService>();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.calledWith(accessToken).mockResolvedValue(decodedToken);
@@ -118,7 +121,7 @@ describe("LogInStrategy", () => {
logService,
stateService,
twoFactorService,
passwordGenerationService,
passwordStrengthService,
policyService,
authService
);

View File

@@ -11,7 +11,10 @@ import { PlatformUtilsService } from "../../platform/abstractions/platform-utils
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordGenerationService } from "../../tools/generator/password";
import {
PasswordStrengthService,
PasswordStrengthServiceAbstraction,
} from "../../tools/password-strength";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -51,7 +54,7 @@ describe("PasswordLogInStrategy", () => {
let twoFactorService: MockProxy<TwoFactorService>;
let authService: MockProxy<AuthService>;
let policyService: MockProxy<PolicyService>;
let passwordGenerationService: MockProxy<PasswordGenerationService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
@@ -68,7 +71,7 @@ describe("PasswordLogInStrategy", () => {
twoFactorService = mock<TwoFactorService>();
authService = mock<AuthService>();
policyService = mock<PolicyService>();
passwordGenerationService = mock<PasswordGenerationService>();
passwordStrengthService = mock<PasswordStrengthService>();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
@@ -94,7 +97,7 @@ describe("PasswordLogInStrategy", () => {
logService,
stateService,
twoFactorService,
passwordGenerationService,
passwordStrengthService,
policyService,
authService
);
@@ -141,7 +144,7 @@ describe("PasswordLogInStrategy", () => {
});
it("does not force the user to update their master password when it meets requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 5 } as any);
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 5 } as any);
policyService.evaluateMasterPassword.mockReturnValue(true);
const result = await passwordLogInStrategy.logIn(credentials);
@@ -151,7 +154,7 @@ describe("PasswordLogInStrategy", () => {
});
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 0 } as any);
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
const result = await passwordLogInStrategy.logIn(credentials);
@@ -164,7 +167,7 @@ describe("PasswordLogInStrategy", () => {
});
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 0 } as any);
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
const token2FAResponse = new IdentityTwoFactorResponse({

View File

@@ -9,7 +9,7 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "../../tools/generator/password";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -54,7 +54,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
logService: LogService,
protected stateService: StateService,
twoFactorService: TwoFactorService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private policyService: PolicyService,
private authService: AuthService
) {
@@ -158,7 +158,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
{ masterPassword, email }: PasswordLogInCredentials,
options: MasterPasswordPolicyOptions
): boolean {
const passwordStrength = this.passwordGenerationService.passwordStrength(
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
email
)?.score;

View File

@@ -17,7 +17,7 @@ import { PlatformUtilsService } from "../../platform/abstractions/platform-utils
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "../../tools/generator/password";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
@@ -102,7 +102,7 @@ export class AuthService implements AuthServiceAbstraction {
protected twoFactorService: TwoFactorService,
protected i18nService: I18nService,
protected encryptService: EncryptService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected policyService: PolicyService
) {}
@@ -133,7 +133,7 @@ export class AuthService implements AuthServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.passwordGenerationService,
this.passwordStrengthService,
this.policyService,
this
);

View File

@@ -1,5 +1,5 @@
import { Collection as CollectionDomain } from "../../admin-console/models/domain/collection";
import { CollectionView } from "../../admin-console/models/view/collection.view";
import { Collection as CollectionDomain } from "../../vault/models/domain/collection";
import { CollectionView } from "../../vault/models/view/collection.view";
import { CollectionExport } from "./collection.export";

View File

@@ -1,6 +1,6 @@
import { Collection as CollectionDomain } from "../../admin-console/models/domain/collection";
import { CollectionView } from "../../admin-console/models/view/collection.view";
import { EncString } from "../../platform/models/domain/enc-string";
import { Collection as CollectionDomain } from "../../vault/models/domain/collection";
import { CollectionView } from "../../vault/models/view/collection.view";
export class CollectionExport {
static template(): CollectionExport {

View File

@@ -1,5 +1,5 @@
import { CollectionWithIdRequest } from "../../admin-console/models/request/collection-with-id.request";
import { CipherRequest } from "../../vault/models/request/cipher.request";
import { CollectionWithIdRequest } from "../../vault/models/request/collection-with-id.request";
import { KvpRequest } from "./kvp.request";

View File

@@ -24,7 +24,7 @@ export abstract class CryptoService {
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
getPublicKey: () => Promise<ArrayBuffer>;
getPrivateKey: () => Promise<ArrayBuffer>;
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
getFingerprint: (fingerprintMaterial: string, publicKey?: ArrayBuffer) => Promise<string[]>;
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;

View File

@@ -17,8 +17,17 @@ export type PayPalConfig = {
buttonAction?: string;
};
export enum Region {
US = "US",
EU = "EU",
SelfHosted = "Self-hosted",
}
export abstract class EnvironmentService {
urls: Observable<Urls>;
urls: Observable<void>;
usUrls: Urls;
euUrls: Urls;
selectedRegion?: Region;
hasBaseUrl: () => boolean;
getNotificationsUrl: () => string;
@@ -32,8 +41,10 @@ export abstract class EnvironmentService {
getScimUrl: () => string;
setUrlsFromStorage: () => Promise<void>;
setUrls: (urls: Urls) => Promise<Urls>;
setRegion: (region: Region) => Promise<void>;
getUrls: () => Urls;
isCloud: () => boolean;
isEmpty: () => boolean;
/**
* @remarks For desktop and browser use only.
* For web, use PlatformUtilsService.isSelfHost()

View File

@@ -1,13 +0,0 @@
import { AbstractControl } from "@angular/forms";
export interface AllValidationErrors {
controlName: string;
errorName: string;
}
export interface FormGroupControls {
[key: string]: AbstractControl;
}
export abstract class FormValidationErrorsService {
getFormValidationErrors: (controls: FormGroupControls) => AllValidationErrors[];
}

View File

@@ -1,12 +1,10 @@
import { Observable } from "rxjs";
import { CollectionData } from "../../admin-console/models/data/collection.data";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { OrganizationData } from "../../admin-console/models/data/organization.data";
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { CollectionView } from "../../admin-console/models/view/collection.view";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
@@ -18,9 +16,11 @@ import { GeneratedPasswordHistory } from "../../tools/generator/password";
import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view";
import { CipherData } from "../../vault/models/data/cipher.data";
import { CollectionData } from "../../vault/models/data/collection.data";
import { FolderData } from "../../vault/models/data/folder.data";
import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view";
import { CollectionView } from "../../vault/models/view/collection.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { ServerConfigData } from "../models/data/server-config.data";
import { Account, AccountSettingsSettings } from "../models/domain/account";
@@ -263,6 +263,8 @@ export abstract class StateService<T extends Account = Account> {
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
getRegion: (options?: StorageOptions) => Promise<string>;
setRegion: (value: string, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;

View File

@@ -1,12 +1,10 @@
import { Jsonify } from "type-fest";
import { CollectionData } from "../../../admin-console/models/data/collection.data";
import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data";
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
import { PolicyData } from "../../../admin-console/models/data/policy.data";
import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { Policy } from "../../../admin-console/models/domain/policy";
import { CollectionView } from "../../../admin-console/models/view/collection.view";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
@@ -17,8 +15,10 @@ import { SendData } from "../../../tools/send/models/data/send.data";
import { SendView } from "../../../tools/send/models/view/send.view";
import { DeepJsonify } from "../../../types/deep-jsonify";
import { CipherData } from "../../../vault/models/data/cipher.data";
import { CollectionData } from "../../../vault/models/data/collection.data";
import { FolderData } from "../../../vault/models/data/folder.data";
import { CipherView } from "../../../vault/models/view/cipher.view";
import { CollectionView } from "../../../vault/models/view/collection.view";
import { Utils } from "../../misc/utils";
import { ServerConfigData } from "../../models/data/server-config.data";
@@ -233,6 +233,7 @@ export class AccountSettings {
approveLoginRequests?: boolean;
avatarColor?: string;
activateAutoFillOnPageLoadFromPolicy?: boolean;
region?: string;
smOnboardingTasks?: Record<string, Record<string, boolean>>;
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {

View File

@@ -36,4 +36,5 @@ export class GlobalState {
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean;
region?: string;
}

View File

@@ -1,5 +1,4 @@
import { Injectable, OnDestroy } from "@angular/core";
import { BehaviorSubject, Subject, concatMap, from, takeUntil, timer } from "rxjs";
import { BehaviorSubject, concatMap, from, timer } from "rxjs";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@@ -11,11 +10,9 @@ import { EnvironmentService } from "../../abstractions/environment.service";
import { StateService } from "../../abstractions/state.service";
import { ServerConfigData } from "../../models/data/server-config.data";
@Injectable()
export class ConfigService implements ConfigServiceAbstraction, OnDestroy {
export class ConfigService implements ConfigServiceAbstraction {
protected _serverConfig = new BehaviorSubject<ServerConfig | null>(null);
serverConfig$ = this._serverConfig.asObservable();
private destroy$ = new Subject<void>();
constructor(
private stateService: StateService,
@@ -30,16 +27,11 @@ export class ConfigService implements ConfigServiceAbstraction, OnDestroy {
this._serverConfig.next(serverConfig);
});
this.environmentService.urls.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.environmentService.urls.subscribe(() => {
this.fetchServerConfig();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async fetchServerConfig(): Promise<ServerConfig> {
try {
const response = await this.configApiService.get();

View File

@@ -204,7 +204,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return privateKey;
}
async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise<string[]> {
async getFingerprint(fingerprintMaterial: string, publicKey?: ArrayBuffer): Promise<string[]> {
if (publicKey == null) {
publicKey = await this.getPublicKey();
}
@@ -214,7 +214,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
keyFingerprint,
userId,
fingerprintMaterial,
32,
"sha256"
);

View File

@@ -3,13 +3,15 @@ import { concatMap, Observable, Subject } from "rxjs";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import {
EnvironmentService as EnvironmentServiceAbstraction,
Region,
Urls,
} from "../abstractions/environment.service";
import { StateService } from "../abstractions/state.service";
export class EnvironmentService implements EnvironmentServiceAbstraction {
private readonly urlsSubject = new Subject<Urls>();
urls: Observable<Urls> = this.urlsSubject;
private readonly urlsSubject = new Subject<void>();
urls: Observable<void> = this.urlsSubject.asObservable();
selectedRegion?: Region;
protected baseUrl: string;
protected webVaultUrl: string;
@@ -21,6 +23,28 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
private keyConnectorUrl: string;
private scimUrl: string = null;
readonly usUrls: Urls = {
base: null,
api: "https://api.bitwarden.com",
identity: "https://identity.bitwarden.com",
icons: "https://icons.bitwarden.net",
webVault: "https://vault.bitwarden.com",
notifications: "https://notifications.bitwarden.com",
events: "https://events.bitwarden.com",
scim: "https://scim.bitwarden.com/v2",
};
readonly euUrls: Urls = {
base: null,
api: "https://api.bitwarden.eu",
identity: "https://identity.bitwarden.eu",
icons: "https://icons.bitwarden.eu",
webVault: "https://vault.bitwarden.eu",
notifications: "https://notifications.bitwarden.eu",
events: "https://events.bitwarden.eu",
scim: "https://scim.bitwarden.eu/v2",
};
constructor(private stateService: StateService) {
this.stateService.activeAccount$
.pipe(
@@ -127,18 +151,42 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
}
async setUrlsFromStorage(): Promise<void> {
const urls: any = await this.stateService.getEnvironmentUrls();
const region = await this.stateService.getRegion();
const savedUrls = 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;
// scimUrl is not saved to storage
// fix environment urls for old users
if (savedUrls.base === "https://vault.bitwarden.com") {
this.setRegion(Region.US);
return;
}
if (savedUrls.base === "https://vault.bitwarden.eu") {
this.setRegion(Region.EU);
return;
}
switch (region) {
case Region.EU:
this.setRegion(Region.EU);
return;
case Region.US:
this.setRegion(Region.US);
return;
case Region.SelfHosted:
default:
this.baseUrl = envUrls.base = savedUrls.base;
this.webVaultUrl = savedUrls.webVault;
this.apiUrl = envUrls.api = savedUrls.api;
this.identityUrl = envUrls.identity = savedUrls.identity;
this.iconsUrl = savedUrls.icons;
this.notificationsUrl = savedUrls.notifications;
this.eventsUrl = envUrls.events = savedUrls.events;
this.keyConnectorUrl = savedUrls.keyConnector;
// scimUrl is not saved to storage
this.urlsSubject.next();
this.setRegion(Region.SelfHosted);
break;
}
}
async setUrls(urls: Urls): Promise<Urls> {
@@ -176,7 +224,9 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
this.keyConnectorUrl = urls.keyConnector;
this.scimUrl = urls.scim;
this.urlsSubject.next(urls);
await this.setRegion(Region.SelfHosted);
this.urlsSubject.next();
return urls;
}
@@ -195,6 +245,52 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
};
}
isEmpty(): boolean {
return (
this.baseUrl == null &&
this.webVaultUrl == null &&
this.apiUrl == null &&
this.identityUrl == null &&
this.iconsUrl == null &&
this.notificationsUrl == null &&
this.eventsUrl == null
);
}
async setRegion(region: Region) {
this.selectedRegion = region;
await this.stateService.setRegion(region);
switch (region) {
case Region.EU:
this.setUrlsInternal(this.euUrls);
break;
case Region.US:
this.setUrlsInternal(this.usUrls);
break;
case Region.SelfHosted:
// if user saves with empty fields, default to US
if (this.isEmpty()) {
this.setRegion(Region.US);
}
break;
}
}
private setUrlsInternal(urls: Urls) {
this.baseUrl = this.formatUrl(urls.base);
this.webVaultUrl = this.formatUrl(urls.webVault);
this.apiUrl = this.formatUrl(urls.api);
this.identityUrl = this.formatUrl(urls.identity);
this.iconsUrl = this.formatUrl(urls.icons);
this.notificationsUrl = this.formatUrl(urls.notifications);
this.eventsUrl = this.formatUrl(urls.events);
this.keyConnectorUrl = this.formatUrl(urls.keyConnector);
// scimUrl cannot be cleared
this.scimUrl = this.formatUrl(urls.scim) ?? this.scimUrl;
this.urlsSubject.next();
}
private formatUrl(url: string): string {
if (url == null || url === "") {
return null;
@@ -209,9 +305,12 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
}
isCloud(): boolean {
return ["https://api.bitwarden.com", "https://vault.bitwarden.com/api"].includes(
this.getApiUrl()
);
return [
"https://api.bitwarden.com",
"https://vault.bitwarden.com/api",
"https://api.bitwarden.eu",
"https://vault.bitwarden.eu/api",
].includes(this.getApiUrl());
}
isSelfHosted(): boolean {

View File

@@ -1,31 +0,0 @@
import { UntypedFormGroup, ValidationErrors } from "@angular/forms";
import {
FormGroupControls,
FormValidationErrorsService as FormValidationErrorsAbstraction,
AllValidationErrors,
} from "../abstractions/form-validation-errors.service";
export class FormValidationErrorsService implements FormValidationErrorsAbstraction {
getFormValidationErrors(controls: FormGroupControls): AllValidationErrors[] {
let errors: AllValidationErrors[] = [];
Object.keys(controls).forEach((key) => {
const control = controls[key];
if (control instanceof UntypedFormGroup) {
errors = errors.concat(this.getFormValidationErrors(control.controls));
}
const controlErrors: ValidationErrors = controls[key].errors;
if (controlErrors !== null) {
Object.keys(controlErrors).forEach((keyError) => {
errors.push({
controlName: key,
errorName: keyError,
});
});
}
});
return errors;
}
}

View File

@@ -1,4 +1,3 @@
import { CollectionData } from "../../admin-console/models/data/collection.data";
import { OrganizationData } from "../../admin-console/models/data/organization.data";
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
@@ -9,6 +8,7 @@ import { EventData } from "../../models/data/event.data";
import { GeneratedPasswordHistory } from "../../tools/generator/password";
import { SendData } from "../../tools/send/models/data/send.data";
import { CipherData } from "../../vault/models/data/cipher.data";
import { CollectionData } from "../../vault/models/data/collection.data";
import { FolderData } from "../../vault/models/data/folder.data";
import { AbstractStorageService } from "../abstractions/storage.service";
import { StateFactory } from "../factories/state-factory";

View File

@@ -1,13 +1,11 @@
import { BehaviorSubject, concatMap } from "rxjs";
import { Jsonify, JsonValue } from "type-fest";
import { CollectionData } from "../../admin-console/models/data/collection.data";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { OrganizationData } from "../../admin-console/models/data/organization.data";
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { CollectionView } from "../../admin-console/models/view/collection.view";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
@@ -26,9 +24,11 @@ import { GeneratedPasswordHistory } from "../../tools/generator/password";
import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view";
import { CipherData } from "../../vault/models/data/cipher.data";
import { CollectionData } from "../../vault/models/data/collection.data";
import { FolderData } from "../../vault/models/data/folder.data";
import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view";
import { CollectionView } from "../../vault/models/view/collection.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { LogService } from "../abstractions/log.service";
import { StateMigrationService } from "../abstractions/state-migration.service";
@@ -1598,6 +1598,28 @@ export class StateService<
);
}
async getRegion(options?: StorageOptions): Promise<string> {
if ((await this.state())?.activeUserId == null) {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getGlobals(options)).region ?? null;
}
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getAccount(options))?.settings?.region ?? null;
}
async setRegion(value: string, options?: StorageOptions): Promise<void> {
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
// This is to allow setting region values before an account is active, while still allowing individual accounts to have their own region.
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
globals.region = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))

View File

@@ -1,4 +1,3 @@
import { Injectable } from "@angular/core";
import {
HttpTransportType,
HubConnection,
@@ -17,7 +16,6 @@ import {
NotificationResponse,
} from "./../models/response/notification.response";
@Injectable()
export class AnonymousHubService implements AnonymousHubServiceAbstraction {
private anonHubConnection: HubConnection;
private url: string;

View File

@@ -1,6 +1,5 @@
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
import { OrganizationConnectionType } from "../admin-console/enums";
import { CollectionRequest } from "../admin-console/models/request/collection.request";
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/request/organization/organization-sponsorship-redeem.request";
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
@@ -15,10 +14,6 @@ import { ProviderUserConfirmRequest } from "../admin-console/models/request/prov
import { ProviderUserInviteRequest } from "../admin-console/models/request/provider/provider-user-invite.request";
import { ProviderUserUpdateRequest } from "../admin-console/models/request/provider/provider-user-update.request";
import { SelectionReadOnlyRequest } from "../admin-console/models/request/selection-read-only.request";
import {
CollectionAccessDetailsResponse,
CollectionResponse,
} from "../admin-console/models/response/collection.response";
import {
OrganizationConnectionConfigApis,
OrganizationConnectionResponse,
@@ -144,9 +139,14 @@ import { CipherCreateRequest } from "../vault/models/request/cipher-create.reque
import { CipherPartialRequest } from "../vault/models/request/cipher-partial.request";
import { CipherShareRequest } from "../vault/models/request/cipher-share.request";
import { CipherRequest } from "../vault/models/request/cipher.request";
import { CollectionRequest } from "../vault/models/request/collection.request";
import { AttachmentUploadDataResponse } from "../vault/models/response/attachment-upload-data.response";
import { AttachmentResponse } from "../vault/models/response/attachment.response";
import { CipherResponse } from "../vault/models/response/cipher.response";
import {
CollectionAccessDetailsResponse,
CollectionResponse,
} from "../vault/models/response/collection.response";
import { SyncResponse } from "../vault/models/response/sync.response";
/**

View File

@@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { CollectionService } from "../../admin-console/abstractions/collection.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { KeyConnectorService } from "../../auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
@@ -13,6 +12,7 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { CipherService } from "../../vault/abstractions/cipher.service";
import { CollectionService } from "../../vault/abstractions/collection.service";
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {

View File

@@ -1,5 +1,3 @@
import * as zxcvbn from "zxcvbn";
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
import { GeneratedPasswordHistory } from "./generated-password-history";
@@ -17,11 +15,6 @@ export abstract class PasswordGenerationServiceAbstraction {
getHistory: () => Promise<GeneratedPasswordHistory[]>;
addHistory: (password: string) => Promise<void>;
clear: (userId?: string) => Promise<void>;
passwordStrength: (
password: string,
email?: string,
userInputs?: string[]
) => zxcvbn.ZXCVBNResult;
normalizeOptions: (
options: PasswordGeneratorOptions,
enforcedPolicyOptions: PasswordGeneratorPolicyOptions

View File

@@ -1,5 +1,3 @@
import * as zxcvbn from "zxcvbn";
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../../admin-console/enums";
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
@@ -387,33 +385,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
}
/**
* Calculates a password strength score using zxcvbn.
* @param password The password to calculate the strength of.
* @param emailInput An unparsed email address to use as user input.
* @param userInputs An array of additional user inputs to use when calculating the strength.
*/
passwordStrength(
password: string,
emailInput: string = null,
userInputs: string[] = null
): zxcvbn.ZXCVBNResult {
if (password == null || password.length === 0) {
return null;
}
const globalUserInputs = [
"bitwarden",
"bit",
"warden",
...(userInputs ?? []),
...this.emailToUserInputs(emailInput),
];
// 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: PasswordGeneratorOptions,
enforcedPolicyOptions: PasswordGeneratorPolicyOptions
@@ -476,27 +447,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
this.sanitizePasswordLength(options, false);
}
/**
* Convert an email address into a list of user inputs for zxcvbn by
* taking the local part of the email address and splitting it into words.
* @param email
* @private
*/
private emailToUserInputs(email: string): string[] {
if (email == null || email.length === 0) {
return [];
}
const atPosition = email.indexOf("@");
if (atPosition < 0) {
return [];
}
return email
.substring(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/);
}
private capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

View File

@@ -18,6 +18,7 @@ export class AnonAddyForwarder implements Forwarder {
headers: new Headers({
Authorization: "Bearer " + options.apiKey,
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
}),
};
const url = "https://app.anonaddy.com/api/v1/aliases";

View File

@@ -0,0 +1,49 @@
import { ApiService } from "../../../../abstractions/api.service";
import { Utils } from "../../../../platform/misc/utils";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarder-options";
export class ForwardEmailForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
if (options.apiKey == null || options.apiKey === "") {
throw "Invalid Forward Email API key.";
}
if (options.forwardemail?.domain == null || options.forwardemail.domain === "") {
throw "Invalid Forward Email domain.";
}
const requestInit: RequestInit = {
redirect: "manual",
cache: "no-store",
method: "POST",
headers: new Headers({
Authorization: "Basic " + Utils.fromUtf8ToB64(options.apiKey + ":"),
"Content-Type": "application/json",
}),
};
const url = `https://api.forwardemail.net/v1/domains/${options.forwardemail.domain}/aliases`;
requestInit.body = JSON.stringify({
labels: options.website,
description:
(options.website != null ? "Website: " + options.website + ". " : "") +
"Generated by Bitwarden.",
});
const request = new Request(url, requestInit);
const response = await apiService.nativeFetch(request);
if (response.status === 200 || response.status === 201) {
const json = await response.json();
return json?.name + "@" + (json?.domain?.name || options.forwardemail.domain);
}
if (response.status === 401) {
throw "Invalid Forward Email API key.";
}
const json = await response.json();
if (json?.message != null) {
throw "Forward Email error:\n" + json.message;
}
if (json?.error != null) {
throw "Forward Email error:\n" + json.error;
}
throw "Unknown Forward Email error occurred.";
}
}

View File

@@ -3,6 +3,7 @@ export class ForwarderOptions {
website: string;
fastmail = new FastmailForwarderOptions();
anonaddy = new AnonAddyForwarderOptions();
forwardemail = new ForwardEmailForwarderOptions();
}
export class FastmailForwarderOptions {
@@ -12,3 +13,7 @@ export class FastmailForwarderOptions {
export class AnonAddyForwarderOptions {
domain: string;
}
export class ForwardEmailForwarderOptions {
domain: string;
}

View File

@@ -5,3 +5,4 @@ export { FirefoxRelayForwarder } from "./firefox-relay-forwarder";
export { Forwarder } from "./forwarder";
export { ForwarderOptions } from "./forwarder-options";
export { SimpleLoginForwarder } from "./simple-login-forwarder";
export { ForwardEmailForwarder } from "./forward-email-forwarder";

View File

@@ -35,13 +35,9 @@ export class SimpleLoginForwarder implements Forwarder {
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...
const json = await response.json();
if (json?.error != null) {
throw "SimpleLogin error:" + json.error;
}
throw "Unknown SimpleLogin error occurred.";
}

View File

@@ -8,6 +8,7 @@ import {
DuckDuckGoForwarder,
FastmailForwarder,
FirefoxRelayForwarder,
ForwardEmailForwarder,
Forwarder,
ForwarderOptions,
SimpleLoginForwarder,
@@ -22,6 +23,7 @@ const DefaultOptions = {
catchallType: "random",
forwardedService: "",
forwardedAnonAddyDomain: "anonaddy.me",
forwardedForwardEmailDomain: "hideaddress.net",
};
export class UsernameGenerationService implements UsernameGenerationServiceAbstraction {
@@ -137,6 +139,10 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr
} else if (o.forwardedService === "duckduckgo") {
forwarder = new DuckDuckGoForwarder();
forwarderOptions.apiKey = o.forwardedDuckDuckGoToken;
} else if (o.forwardedService === "forwardemail") {
forwarder = new ForwardEmailForwarder();
forwarderOptions.apiKey = o.forwardedForwardEmailApiToken;
forwarderOptions.forwardemail.domain = o.forwardedForwardEmailDomain;
}
if (forwarder == null) {

View File

@@ -0,0 +1,2 @@
export { PasswordStrengthServiceAbstraction } from "./password-strength.service.abstraction";
export { PasswordStrengthService } from "./password-strength.service";

View File

@@ -0,0 +1,5 @@
import { ZXCVBNResult } from "zxcvbn";
export abstract class PasswordStrengthServiceAbstraction {
getPasswordStrength: (password: string, email?: string, userInputs?: string[]) => ZXCVBNResult;
}

View File

@@ -0,0 +1,53 @@
import * as zxcvbn from "zxcvbn";
import { PasswordStrengthServiceAbstraction } from "./password-strength.service.abstraction";
export class PasswordStrengthService implements PasswordStrengthServiceAbstraction {
/**
* Calculates a password strength score using zxcvbn.
* @param password The password to calculate the strength of.
* @param emailInput An unparsed email address to use as user input.
* @param userInputs An array of additional user inputs to use when calculating the strength.
*/
getPasswordStrength(
password: string,
emailInput: string = null,
userInputs: string[] = null
): zxcvbn.ZXCVBNResult {
if (password == null || password.length === 0) {
return null;
}
const globalUserInputs = [
"bitwarden",
"bit",
"warden",
...(userInputs ?? []),
...this.emailToUserInputs(emailInput),
];
// 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;
}
/**
* Convert an email address into a list of user inputs for zxcvbn by
* taking the local part of the email address and splitting it into words.
* @param email
* @private
*/
private emailToUserInputs(email: string): string[] {
if (email == null || email.length === 0) {
return [];
}
const atPosition = email.indexOf("@");
if (atPosition < 0) {
return [];
}
return email
.substring(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/);
}
}

View File

@@ -1,5 +1,6 @@
import { Collection } from "../domain/collection";
import { CollectionRequest } from "../request/collection.request";
import { CollectionRequest } from "./collection.request";
export class CollectionWithIdRequest extends CollectionRequest {
id: string;

View File

@@ -1,7 +1,6 @@
import { SelectionReadOnlyRequest } from "../../../admin-console/models/request/selection-read-only.request";
import { Collection } from "../domain/collection";
import { SelectionReadOnlyRequest } from "./selection-read-only.request";
export class CollectionRequest {
name: string;
externalId: string;

View File

@@ -1,7 +1,6 @@
import { SelectionReadOnlyResponse } from "../../../admin-console/models/response/selection-read-only.response";
import { BaseResponse } from "../../../models/response/base.response";
import { SelectionReadOnlyResponse } from "./selection-read-only.response";
export class CollectionResponse extends BaseResponse {
id: string;
organizationId: string;

View File

@@ -1,4 +1,3 @@
import { CollectionDetailsResponse } from "../../../admin-console/models/response/collection.response";
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
import { BaseResponse } from "../../../models/response/base.response";
import { DomainsResponse } from "../../../models/response/domains.response";
@@ -6,6 +5,7 @@ import { ProfileResponse } from "../../../models/response/profile.response";
import { SendResponse } from "../../../tools/send/models/response/send.response";
import { CipherResponse } from "./cipher.response";
import { CollectionDetailsResponse } from "./collection.response";
import { FolderResponse } from "./folder.response";
export class SyncResponse extends BaseResponse {

View File

@@ -81,4 +81,67 @@ export class CardView extends ItemView {
static fromJSON(obj: Partial<Jsonify<CardView>>): CardView {
return Object.assign(new CardView(), obj);
}
// ref https://stackoverflow.com/a/5911300
static getCardBrandByPatterns(cardNum: string): string {
if (cardNum == null || typeof cardNum !== "string" || cardNum.trim() === "") {
return null;
}
// Visa
let re = new RegExp("^4");
if (cardNum.match(re) != null) {
return "Visa";
}
// Mastercard
// Updated for Mastercard 2017 BINs expansion
if (
/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test(
cardNum
)
) {
return "Mastercard";
}
// AMEX
re = new RegExp("^3[47]");
if (cardNum.match(re) != null) {
return "Amex";
}
// Discover
re = new RegExp(
"^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)"
);
if (cardNum.match(re) != null) {
return "Discover";
}
// Diners
re = new RegExp("^36");
if (cardNum.match(re) != null) {
return "Diners Club";
}
// Diners - Carte Blanche
re = new RegExp("^30[0-5]");
if (cardNum.match(re) != null) {
return "Diners Club";
}
// JCB
re = new RegExp("^35(2[89]|[3-8][0-9])");
if (cardNum.match(re) != null) {
return "JCB";
}
// Visa Electron
re = new RegExp("^(4026|417500|4508|4844|491(3|7))");
if (cardNum.match(re) != null) {
return "Visa";
}
return null;
}
}

View File

@@ -4,7 +4,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { CollectionService as CollectionServiceAbstraction } from "../abstractions/collection.service";
import { CollectionService as CollectionServiceAbstraction } from "../../vault/abstractions/collection.service";
import { CollectionData } from "../models/data/collection.data";
import { Collection } from "../models/domain/collection";
import { CollectionView } from "../models/view/collection.view";

View File

@@ -1,14 +1,11 @@
import { ApiService } from "../../../abstractions/api.service";
import { SettingsService } from "../../../abstractions/settings.service";
import { CollectionService } from "../../../admin-console/abstractions/collection.service";
import { InternalOrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "../../../admin-console/abstractions/provider.service";
import { CollectionData } from "../../../admin-console/models/data/collection.data";
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
import { PolicyData } from "../../../admin-console/models/data/policy.data";
import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { CollectionDetailsResponse } from "../../../admin-console/models/response/collection.response";
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
@@ -36,6 +33,9 @@ import { CipherData } from "../../../vault/models/data/cipher.data";
import { FolderData } from "../../../vault/models/data/folder.data";
import { CipherResponse } from "../../../vault/models/response/cipher.response";
import { FolderResponse } from "../../../vault/models/response/folder.response";
import { CollectionService } from "../../abstractions/collection.service";
import { CollectionData } from "../../models/data/collection.data";
import { CollectionDetailsResponse } from "../../models/response/collection.response";
export class SyncService implements SyncServiceAbstraction {
syncInProgress = false;