1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-24 04:04:24 +00:00

Merge branch 'main' into vault/pm-5273

# Conflicts:
#	apps/browser/src/popup/app.component.ts
#	libs/common/src/state-migrations/migrate.ts
This commit is contained in:
Carlos Gonçalves
2024-03-12 20:05:58 +00:00
129 changed files with 3197 additions and 1250 deletions

View File

@@ -1,14 +1,8 @@
import { Observable } from "rxjs";
import { AccountSettingsSettings } from "../platform/models/domain/account";
export abstract class SettingsService {
settings$: Observable<AccountSettingsSettings>;
disableFavicon$: Observable<boolean>;
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
getEquivalentDomains: (url: string) => Set<string>;
setDisableFavicon: (value: boolean) => Promise<any>;
getDisableFavicon: () => boolean;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -1,7 +1,7 @@
export enum AuthenticationType {
Password = 0,
Sso = 1,
UserApi = 2,
UserApiKey = 2,
AuthRequest = 3,
WebAuthn = 4,
}

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { DeviceType } from "../../../../enums";
import { PlatformUtilsService } from "../../../../platform/abstractions/platform-utils.service";
@@ -13,4 +15,8 @@ export class DeviceRequest {
this.identifier = appId;
this.pushToken = null;
}
static fromJSON(json: Jsonify<DeviceRequest>) {
return Object.assign(Object.create(DeviceRequest.prototype), json);
}
}

View File

@@ -34,4 +34,13 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
alterIdentityTokenHeaders(headers: Headers) {
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
}
static fromJSON(json: any) {
return Object.assign(Object.create(PasswordTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}

View File

@@ -23,4 +23,13 @@ export class SsoTokenRequest extends TokenRequest {
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(SsoTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}

View File

@@ -21,4 +21,13 @@ export class UserApiTokenRequest extends TokenRequest {
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(UserApiTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}

View File

@@ -1,6 +1,7 @@
import { WebAuthnLoginAssertionResponseRequest } from "../../../services/webauthn-login/request/webauthn-login-assertion-response.request";
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class WebAuthnLoginTokenRequest extends TokenRequest {
@@ -22,4 +23,14 @@ export class WebAuthnLoginTokenRequest extends TokenRequest {
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(WebAuthnLoginTokenRequest.prototype), json, {
deviceResponse: WebAuthnLoginAssertionResponseRequest.fromJSON(json.deviceResponse),
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}

View File

@@ -54,7 +54,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction {
}
private async ProcessNotification(notification: NotificationResponse) {
await this.loginStrategyService.authResponsePushNotification(
await this.loginStrategyService.sendAuthRequestPushNotification(
notification.payload as AuthRequestPushNotification,
);
}

View File

@@ -1,17 +1,22 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { UserId } from "../../../../common/src/types/guid";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { AccountInfo, AccountService } from "../abstractions/account.service";
import { AuthenticationStatus } from "../enums/authentication-status";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
describe("PasswordResetEnrollmentServiceImplementation", () => {
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let stateService: MockProxy<StateService>;
let accountService: MockProxy<AccountService>;
let cryptoService: MockProxy<CryptoService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let i18nService: MockProxy<I18nService>;
@@ -19,13 +24,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
beforeEach(() => {
organizationApiService = mock<OrganizationApiServiceAbstraction>();
stateService = mock<StateService>();
accountService = mock<AccountService>();
accountService.activeAccount$ = activeAccountSubject;
cryptoService = mock<CryptoService>();
organizationUserService = mock<OrganizationUserService>();
i18nService = mock<I18nService>();
service = new PasswordResetEnrollmentServiceImplementation(
organizationApiService,
stateService,
accountService,
cryptoService,
organizationUserService,
i18nService,
@@ -81,7 +87,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
};
const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
stateService.getUserId.mockResolvedValue("userId");
const user1AccountInfo: AccountInfo = {
name: "Test User 1",
email: "test1@email.com",
status: AuthenticationStatus.Unlocked,
};
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);

View File

@@ -1,11 +1,13 @@
import { firstValueFrom, map } from "rxjs";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../admin-console/abstractions/organization-user/requests";
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 { UserKey } from "../../types/key";
import { AccountService } from "../abstractions/account.service";
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
export class PasswordResetEnrollmentServiceImplementation
@@ -13,7 +15,7 @@ export class PasswordResetEnrollmentServiceImplementation
{
constructor(
protected organizationApiService: OrganizationApiServiceAbstraction,
protected stateService: StateService,
protected accountService: AccountService,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected i18nService: I18nService,
@@ -38,7 +40,8 @@ export class PasswordResetEnrollmentServiceImplementation
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
userId = userId ?? (await this.stateService.getUserId());
userId =
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
// RSA Encrypt user's userKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey);

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { Utils } from "../../../../platform/misc/utils";
import { WebAuthnLoginResponseRequest } from "./webauthn-login-response.request";
@@ -27,4 +29,8 @@ export class WebAuthnLoginAssertionResponseRequest extends WebAuthnLoginResponse
userHandle: Utils.fromBufferToUrlB64(credential.response.userHandle),
};
}
static fromJSON(json: Jsonify<WebAuthnLoginAssertionResponseRequest>) {
return Object.assign(Object.create(WebAuthnLoginAssertionResponseRequest.prototype), json);
}
}

View File

@@ -0,0 +1,53 @@
import { firstValueFrom, of } from "rxjs";
import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec";
import { Utils } from "../../platform/misc/utils";
import { UserId } from "../../types/guid";
import { DefaultDomainSettingsService, DomainSettingsService } from "./domain-settings.service";
describe("DefaultDomainSettingsService", () => {
let domainSettingsService: DomainSettingsService;
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
beforeEach(() => {
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
jest.spyOn(domainSettingsService, "getUrlEquivalentDomains");
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
});
describe("getUrlEquivalentDomains", () => {
it("returns all equivalent domains for a URL", async () => {
const expected = new Set([
"example.com",
"exampleapp.com",
"example.co.uk",
"ejemplo.es",
"exampleapp.co.uk",
]);
const actual = await firstValueFrom(
domainSettingsService.getUrlEquivalentDomains("example.co.uk"),
);
expect(domainSettingsService.getUrlEquivalentDomains).toHaveBeenCalledWith("example.co.uk");
expect(actual).toEqual(expected);
});
it("returns an empty set if there are no equivalent domains", async () => {
const actual = await firstValueFrom(domainSettingsService.getUrlEquivalentDomains("asdf"));
expect(domainSettingsService.getUrlEquivalentDomains).toHaveBeenCalledWith("asdf");
expect(actual).toEqual(new Set());
});
});
});

View File

@@ -0,0 +1,97 @@
import { map, Observable } from "rxjs";
import {
NeverDomains,
EquivalentDomains,
UriMatchStrategySetting,
UriMatchStrategy,
} from "../../models/domain/domain-service";
import { Utils } from "../../platform/misc/utils";
import {
DOMAIN_SETTINGS_DISK,
ActiveUserState,
GlobalState,
KeyDefinition,
StateProvider,
UserKeyDefinition,
} from "../../platform/state";
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
deserializer: (value: NeverDomains) => value ?? null,
});
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
deserializer: (value: EquivalentDomains) => value ?? null,
clearOn: ["logout"],
});
const DEFAULT_URI_MATCH_STRATEGY = new KeyDefinition(
DOMAIN_SETTINGS_DISK,
"defaultUriMatchStrategy",
{
deserializer: (value: UriMatchStrategySetting) => value ?? UriMatchStrategy.Domain,
},
);
export abstract class DomainSettingsService {
neverDomains$: Observable<NeverDomains>;
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
equivalentDomains$: Observable<EquivalentDomains>;
setEquivalentDomains: (newValue: EquivalentDomains) => Promise<void>;
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
}
export class DefaultDomainSettingsService implements DomainSettingsService {
private neverDomainsState: GlobalState<NeverDomains>;
readonly neverDomains$: Observable<NeverDomains>;
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
readonly equivalentDomains$: Observable<EquivalentDomains>;
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
constructor(private stateProvider: StateProvider) {
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
this.defaultUriMatchStrategyState = this.stateProvider.getActive(DEFAULT_URI_MATCH_STRATEGY);
this.defaultUriMatchStrategy$ = this.defaultUriMatchStrategyState.state$.pipe(
map((x) => x ?? UriMatchStrategy.Domain),
);
}
async setNeverDomains(newValue: NeverDomains): Promise<void> {
await this.neverDomainsState.update(() => newValue);
}
async setEquivalentDomains(newValue: EquivalentDomains): Promise<void> {
await this.equivalentDomainsState.update(() => newValue);
}
async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise<void> {
await this.defaultUriMatchStrategyState.update(() => newValue);
}
getUrlEquivalentDomains(url: string): Observable<Set<string>> {
const domains$ = this.equivalentDomains$.pipe(
map((equivalentDomains) => {
const domain = Utils.getDomain(url);
if (domain == null || equivalentDomains == null) {
return new Set() as Set<string>;
}
const equivalents = equivalentDomains.filter((ed) => ed.includes(domain)).flat();
return new Set(equivalents);
}),
);
return domains$;
}
}

View File

@@ -0,0 +1,24 @@
/*
See full documentation at:
https://bitwarden.com/help/uri-match-detection/#match-detection-options
Domain: "the top-level domain and second-level domain of the URI match the detected resource",
Host: "the hostname and (if specified) port of the URI matches the detected resource",
StartsWith: "the detected resource starts with the URI, regardless of what follows it",
Exact: "the URI matches the detected resource exactly",
RegularExpression: "the detected resource matches a specified regular expression",
Never: "never offer auto-fill for the item",
*/
export const UriMatchStrategy = {
Domain: 0,
Host: 1,
StartsWith: 2,
Exact: 3,
RegularExpression: 4,
Never: 5,
} as const;
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
export type NeverDomains = { [id: string]: unknown };
export type EquivalentDomains = string[][];

View File

@@ -1,5 +1,5 @@
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { EncString } from "../../platform/models/domain/enc-string";
import { UriMatchType } from "../../vault/enums";
import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri";
import { LoginUriView } from "../../vault/models/view/login-uri.view";
@@ -26,7 +26,7 @@ export class LoginUriExport {
uri: string;
uriChecksum: string | undefined;
match: UriMatchType = null;
match: UriMatchStrategySetting = null;
constructor(o?: LoginUriView | LoginUriDomain) {
if (o == null) {

View File

@@ -1,4 +1,8 @@
import { Observable } from "rxjs";
export abstract class AppIdService {
appId$: Observable<string>;
anonymousAppId$: Observable<string>;
getAppId: () => Promise<string>;
getAnonymousAppId: () => Promise<string>;
}

View File

@@ -14,17 +14,12 @@ import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view";
import { UserId } from "../../types/guid";
import { DeviceKey, MasterKey } from "../../types/key";
import { UriMatchType } from "../../vault/enums";
import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view";
import { KdfType, ThemeType } from "../enums";
import { ServerConfigData } from "../models/data/server-config.data";
import {
Account,
AccountDecryptionOptions,
AccountSettingsSettings,
} from "../models/domain/account";
import { Account, AccountDecryptionOptions } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
@@ -184,8 +179,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this directly, use SendService
*/
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this, use SettingsService
*/
@@ -272,8 +265,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this directly, use SendService
*/
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
@@ -307,8 +298,6 @@ export abstract class StateService<T extends Account = Account> {
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>;
setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise<void>;
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
@@ -350,14 +339,6 @@ export abstract class StateService<T extends Account = Account> {
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use SettingsService
*/
getSettings: (options?: StorageOptions) => Promise<AccountSettingsSettings>;
/**
* @deprecated Do not call this directly, use SettingsService
*/
setSettings: (value: AccountSettingsSettings, options?: StorageOptions) => Promise<void>;
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;

View File

@@ -253,11 +253,10 @@ export class Utils {
});
}
static guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
static isGuid(id: string) {
return RegExp(
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
"i",
).test(id);
return RegExp(Utils.guidRegex, "i").test(id);
}
static getHostname(uriString: string): string {

View File

@@ -7,6 +7,7 @@ import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/us
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
import { EventData } from "../../../models/data/event.data";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { GeneratorOptions } from "../../../tools/generator/generator-options";
import {
GeneratedPasswordHistory,
@@ -17,7 +18,6 @@ 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 { MasterKey } from "../../../types/key";
import { UriMatchType } from "../../../vault/enums";
import { CipherData } from "../../../vault/models/data/cipher.data";
import { CipherView } from "../../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info";
@@ -196,13 +196,12 @@ export class AccountProfile {
export class AccountSettings {
autoConfirmFingerPrints?: boolean;
defaultUriMatch?: UriMatchType;
defaultUriMatch?: UriMatchStrategySetting;
disableGa?: boolean;
dontShowCardsCurrentTab?: boolean;
dontShowIdentitiesCurrentTab?: boolean;
enableAlwaysOnTop?: boolean;
enableBiometric?: boolean;
equivalentDomains?: any;
minimizeOnCopyToClipboard?: boolean;
passwordGenerationOptions?: PasswordGeneratorOptions;
usernameGenerationOptions?: UsernameGeneratorOptions;
@@ -210,7 +209,6 @@ export class AccountSettings {
pinKeyEncryptedUserKey?: EncryptedString;
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
protectedPin?: string;
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number;
vaultTimeoutAction?: string = "lock";
serverConfig?: ServerConfigData;
@@ -236,10 +234,6 @@ export class AccountSettings {
}
}
export type AccountSettingsSettings = {
equivalentDomains?: string[][];
};
export class AccountTokens {
accessToken?: string;
refreshToken?: string;

View File

@@ -25,6 +25,5 @@ export class GlobalState {
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean;
neverDomains?: { [id: string]: unknown };
deepLinkRedirectUrl?: string;
}

View File

@@ -0,0 +1,101 @@
import { FakeGlobalStateProvider } from "../../../spec";
import { Utils } from "../misc/utils";
import { ANONYMOUS_APP_ID_KEY, APP_ID_KEY, AppIdService } from "./app-id.service";
describe("AppIdService", () => {
const globalStateProvider = new FakeGlobalStateProvider();
const appIdState = globalStateProvider.getFake(APP_ID_KEY);
const anonymousAppIdState = globalStateProvider.getFake(ANONYMOUS_APP_ID_KEY);
let sut: AppIdService;
beforeEach(() => {
sut = new AppIdService(globalStateProvider);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("getAppId", () => {
it("returns the existing appId when it exists", async () => {
appIdState.stateSubject.next("existingAppId");
const appId = await sut.getAppId();
expect(appId).toBe("existingAppId");
});
it.each([null, undefined])(
"uses the util function to create a new id when it AppId does not exist",
async (value) => {
appIdState.stateSubject.next(value);
const spy = jest.spyOn(Utils, "newGuid");
await sut.getAppId();
expect(spy).toHaveBeenCalledTimes(1);
},
);
it.each([null, undefined])("returns a new appId when it does not exist", async (value) => {
appIdState.stateSubject.next(value);
const appId = await sut.getAppId();
expect(appId).toMatch(Utils.guidRegex);
});
it.each([null, undefined])(
"stores the new guid when it an existing one is not found",
async (value) => {
appIdState.stateSubject.next(value);
const appId = await sut.getAppId();
expect(appIdState.nextMock).toHaveBeenCalledWith(appId);
},
);
});
describe("getAnonymousAppId", () => {
it("returns the existing appId when it exists", async () => {
anonymousAppIdState.stateSubject.next("existingAppId");
const appId = await sut.getAnonymousAppId();
expect(appId).toBe("existingAppId");
});
it.each([null, undefined])(
"uses the util function to create a new id when it AppId does not exist",
async (value) => {
anonymousAppIdState.stateSubject.next(value);
const spy = jest.spyOn(Utils, "newGuid");
await sut.getAnonymousAppId();
expect(spy).toHaveBeenCalledTimes(1);
},
);
it.each([null, undefined])("returns a new appId when it does not exist", async (value) => {
anonymousAppIdState.stateSubject.next(value);
const appId = await sut.getAnonymousAppId();
expect(appId).toMatch(Utils.guidRegex);
});
it.each([null, undefined])(
"stores the new guid when it an existing one is not found",
async (value) => {
anonymousAppIdState.stateSubject.next(value);
const appId = await sut.getAnonymousAppId();
expect(anonymousAppIdState.nextMock).toHaveBeenCalledWith(appId);
},
);
});
});

View File

@@ -1,31 +1,46 @@
import { Observable, filter, firstValueFrom, tap } from "rxjs";
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service";
import { AbstractStorageService } from "../abstractions/storage.service";
import { HtmlStorageLocation } from "../enums";
import { Utils } from "../misc/utils";
import { APPLICATION_ID_DISK, GlobalStateProvider, KeyDefinition } from "../state";
export const APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "appId", {
deserializer: (value: string) => value,
});
export const ANONYMOUS_APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "anonymousAppId", {
deserializer: (value: string) => value,
});
export class AppIdService implements AppIdServiceAbstraction {
constructor(private storageService: AbstractStorageService) {}
appId$: Observable<string>;
anonymousAppId$: Observable<string>;
getAppId(): Promise<string> {
return this.makeAndGetAppId("appId");
constructor(globalStateProvider: GlobalStateProvider) {
const appIdState = globalStateProvider.get(APP_ID_KEY);
const anonymousAppIdState = globalStateProvider.get(ANONYMOUS_APP_ID_KEY);
this.appId$ = appIdState.state$.pipe(
tap(async (appId) => {
if (!appId) {
await appIdState.update(() => Utils.newGuid());
}
}),
filter((appId) => !!appId),
);
this.anonymousAppId$ = anonymousAppIdState.state$.pipe(
tap(async (appId) => {
if (!appId) {
await anonymousAppIdState.update(() => Utils.newGuid());
}
}),
filter((appId) => !!appId),
);
}
getAnonymousAppId(): Promise<string> {
return this.makeAndGetAppId("anonymousAppId");
async getAppId(): Promise<string> {
return await firstValueFrom(this.appId$);
}
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;
async getAnonymousAppId(): Promise<string> {
return await firstValueFrom(this.anonymousAppId$);
}
}

View File

@@ -18,7 +18,6 @@ import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view";
import { UserId } from "../../types/guid";
import { DeviceKey, MasterKey } from "../../types/key";
import { UriMatchType } from "../../vault/enums";
import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view";
@@ -41,7 +40,6 @@ import {
AccountData,
AccountDecryptionOptions,
AccountSettings,
AccountSettingsSettings,
} from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state";
@@ -786,23 +784,6 @@ export class StateService<
);
}
async getDefaultUriMatch(options?: StorageOptions): Promise<UriMatchType> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.defaultUriMatch;
}
async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.defaultUriMatch = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getDisableFavicon(options?: StorageOptions): Promise<boolean> {
return (
(
@@ -1304,23 +1285,6 @@ export class StateService<
);
}
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.equivalentDomains;
}
async setEquivalentDomains(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.equivalentDomains = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
@withPrototypeForArrayMembers(EventData)
async getEventCollection(options?: StorageOptions): Promise<EventData[]> {
return (
@@ -1580,23 +1544,6 @@ export class StateService<
);
}
async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: unknown }> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.neverDomains;
}
async setNeverDomains(value: { [id: string]: unknown }, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.neverDomains = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getOpenAtLogin(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@@ -1778,23 +1725,6 @@ export class StateService<
);
}
async getSettings(options?: StorageOptions): Promise<AccountSettingsSettings> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
)?.settings?.settings;
}
async setSettings(value: AccountSettingsSettings, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
account.settings.settings = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
}
async getTheme(options?: StorageOptions): Promise<ThemeType> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))

View File

@@ -15,6 +15,7 @@ export interface GlobalState<T> {
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/
update: <TCombine>(
configureState: (state: T, dependency: TCombine) => T,

View File

@@ -27,6 +27,7 @@ export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
// Autofill
@@ -38,6 +39,8 @@ export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
// Billing
export const DOMAIN_SETTINGS_DISK = new StateDefinition("domainSettings", "disk");
export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk");
export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", {
web: "disk-local",
@@ -52,6 +55,9 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne
// Platform
export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", {
web: "disk-local",
});
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");

View File

@@ -32,7 +32,8 @@ export interface ActiveUserState<T> extends UserState<T> {
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
* @returns The new state
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/
readonly update: <TCombine>(
configureState: (state: T, dependencies: TCombine) => T,
@@ -50,7 +51,8 @@ export interface SingleUserState<T> extends UserState<T> {
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
* @returns The new state
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/
readonly update: <TCombine>(
configureState: (state: T, dependencies: TCombine) => T,

View File

@@ -1,10 +1,11 @@
import * as lunr from "lunr";
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
import { UriMatchStrategy } from "../models/domain/domain-service";
import { I18nService } from "../platform/abstractions/i18n.service";
import { LogService } from "../platform/abstractions/log.service";
import { SendView } from "../tools/send/models/view/send.view";
import { FieldType, UriMatchType } from "../vault/enums";
import { FieldType } from "../vault/enums";
import { CipherType } from "../vault/enums/cipher-type";
import { CipherView } from "../vault/models/view/cipher.view";
@@ -288,7 +289,7 @@ export class SearchService implements SearchServiceAbstraction {
return;
}
let uri = u.uri;
if (u.match !== UriMatchType.RegularExpression) {
if (u.match !== UriMatchStrategy.RegularExpression) {
const protocolIndex = uri.indexOf("://");
if (protocolIndex > -1) {
uri = uri.substr(protocolIndex + 3);

View File

@@ -1,83 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { CryptoService } from "../platform/abstractions/crypto.service";
import { EncryptService } from "../platform/abstractions/encrypt.service";
import { StateService } from "../platform/abstractions/state.service";
import { ContainerService } from "../platform/services/container.service";
import { SettingsService } from "./settings.service";
describe("SettingsService", () => {
let settingsService: SettingsService;
let cryptoService: MockProxy<CryptoService>;
let encryptService: MockProxy<EncryptService>;
let stateService: MockProxy<StateService>;
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>;
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
beforeEach(() => {
cryptoService = mock<CryptoService>();
encryptService = mock<EncryptService>();
stateService = mock<StateService>();
activeAccount = new BehaviorSubject("123");
activeAccountUnlocked = new BehaviorSubject(true);
stateService.getSettings.mockResolvedValue({ equivalentDomains: mockEquivalentDomains });
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
settingsService = new SettingsService(stateService);
});
afterEach(() => {
activeAccount.complete();
activeAccountUnlocked.complete();
});
describe("getEquivalentDomains", () => {
it("returns all equivalent domains for a URL", async () => {
const actual = settingsService.getEquivalentDomains("example.co.uk");
const expected = new Set([
"example.com",
"exampleapp.com",
"example.co.uk",
"ejemplo.es",
"exampleapp.co.uk",
]);
expect(actual).toEqual(expected);
});
it("returns an empty set if there are no equivalent domains", () => {
const actual = settingsService.getEquivalentDomains("asdf");
expect(actual).toEqual(new Set());
});
});
it("setEquivalentDomains", async () => {
await settingsService.setEquivalentDomains([["test2"], ["domains2"]]);
expect(stateService.setSettings).toBeCalledTimes(1);
expect((await firstValueFrom(settingsService.settings$)).equivalentDomains).toEqual([
["test2"],
["domains2"],
]);
});
it("clear", async () => {
await settingsService.clear();
expect(stateService.setSettings).toBeCalledTimes(1);
expect(await firstValueFrom(settingsService.settings$)).toEqual({});
});
});

View File

@@ -3,13 +3,10 @@ import { BehaviorSubject, concatMap } from "rxjs";
import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service";
import { StateService } from "../platform/abstractions/state.service";
import { Utils } from "../platform/misc/utils";
import { AccountSettingsSettings } from "../platform/models/domain/account";
export class SettingsService implements SettingsServiceAbstraction {
protected _settings: BehaviorSubject<AccountSettingsSettings> = new BehaviorSubject({});
protected _disableFavicon = new BehaviorSubject<boolean>(null);
settings$ = this._settings.asObservable();
disableFavicon$ = this._disableFavicon.asObservable();
constructor(private stateService: StateService) {
@@ -21,50 +18,17 @@ export class SettingsService implements SettingsServiceAbstraction {
}
if (!unlocked) {
this._settings.next({});
return;
}
const data = await this.stateService.getSettings();
const disableFavicon = await this.stateService.getDisableFavicon();
this._settings.next(data);
this._disableFavicon.next(disableFavicon);
}),
)
.subscribe();
}
async setEquivalentDomains(equivalentDomains: string[][]): Promise<void> {
const settings = this._settings.getValue() ?? {};
settings.equivalentDomains = equivalentDomains;
this._settings.next(settings);
await this.stateService.setSettings(settings);
}
getEquivalentDomains(url: string): Set<string> {
const domain = Utils.getDomain(url);
if (domain == null) {
return new Set();
}
const settings = this._settings.getValue();
let result: string[] = [];
if (settings?.equivalentDomains != null) {
settings.equivalentDomains
.filter((ed) => ed.length > 0 && ed.includes(domain))
.forEach((ed) => {
result = result.concat(ed);
});
}
return new Set(result);
}
async setDisableFavicon(value: boolean) {
this._disableFavicon.next(value);
await this.stateService.setDisableFavicon(value);
@@ -73,12 +37,4 @@ export class SettingsService implements SettingsServiceAbstraction {
getDisableFavicon(): boolean {
return this._disableFavicon.getValue();
}
async clear(userId?: string): Promise<void> {
if (userId == null || userId == (await this.stateService.getUserId())) {
this._settings.next({});
}
await this.stateService.setSettings(null, { userId: userId });
}
}

View File

@@ -28,7 +28,9 @@ import { FixPremiumMigrator } from "./migrations/3-fix-premium";
import { PolicyMigrator } from "./migrations/30-move-policy-state-to-state-provider";
import { EnableContextMenuMigrator } from "./migrations/31-move-enable-context-menu-to-autofill-settings-state-provider";
import { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language";
import { LocalDataMigrator } from "./migrations/33-move-local-data-to-state-provider";
import { AppIdMigrator } from "./migrations/33-move-app-id-to-state-providers";
import { DomainSettingsMigrator } from "./migrations/34-move-domain-settings-to-state-providers";
import { LocalDataMigrator } from "./migrations/35-move-local-data-to-state-provider";
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
@@ -38,7 +40,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 2;
export const CURRENT_VERSION = 33;
export const CURRENT_VERSION = 35;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -74,7 +76,9 @@ export function createMigrationBuilder() {
.with(PolicyMigrator, 29, 30)
.with(EnableContextMenuMigrator, 30, 31)
.with(PreferredLanguageMigrator, 31, 32)
.with(LocalDataMigrator, 32, CURRENT_VERSION);
.with(AppIdMigrator, 32, 33)
.with(DomainSettingsMigrator, 33, 34)
.with(LocalDataMigrator, 34, CURRENT_VERSION);
}
export async function currentVersion(

View File

@@ -0,0 +1,213 @@
import { MockProxy, any } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import {
ANONYMOUS_APP_ID_KEY,
APP_ID_KEY,
AppIdMigrator,
} from "./33-move-app-id-to-state-providers";
function exampleJSON() {
return {
appId: "appId",
anonymousAppId: "anonymousAppId",
otherStuff: "otherStuff1",
};
}
function missingAppIdJSON() {
return {
anonymousAppId: "anonymousAppId",
otherStuff: "otherStuff1",
};
}
function missingAnonymousAppIdJSON() {
return {
appId: "appId",
otherStuff: "otherStuff1",
};
}
function missingBothJSON() {
return {
otherStuff: "otherStuff1",
};
}
function rollbackJSON() {
return {
global_applicationId_appId: "appId",
global_applicationId_anonymousAppId: "anonymousAppId",
otherStuff: "otherStuff1",
};
}
describe("AppIdMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: AppIdMigrator;
describe("migrate with both ids", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 32);
sut = new AppIdMigrator(32, 33);
});
it("removes appId", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("appId", null);
});
it("removes anonymousAppId", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("anonymousAppId", null);
});
it("sets appId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(APP_ID_KEY, "appId");
});
it("sets anonymousAppId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, "anonymousAppId");
});
});
describe("migrate with missing appId", () => {
beforeEach(() => {
helper = mockMigrationHelper(missingAppIdJSON(), 32);
sut = new AppIdMigrator(32, 33);
});
it("does not set appId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).not.toHaveBeenCalledWith(APP_ID_KEY, any());
});
it("removes anonymousAppId", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("anonymousAppId", null);
});
it("does not set appId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).not.toHaveBeenCalledWith(APP_ID_KEY, any());
});
it("sets anonymousAppId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, "anonymousAppId");
});
});
describe("migrate with missing anonymousAppId", () => {
beforeEach(() => {
helper = mockMigrationHelper(missingAnonymousAppIdJSON(), 32);
sut = new AppIdMigrator(32, 33);
});
it("sets appId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(APP_ID_KEY, "appId");
});
it("does not set anonymousAppId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).not.toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, any());
});
it("removes appId", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("appId", null);
});
it("does not remove anonymousAppId", async () => {
await sut.migrate(helper);
expect(helper.set).not.toHaveBeenCalledWith("anonymousAppId", any());
});
});
describe("migrate with missing appId and anonymousAppId", () => {
beforeEach(() => {
helper = mockMigrationHelper(missingBothJSON(), 32);
sut = new AppIdMigrator(32, 33);
});
it("does not set appId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).not.toHaveBeenCalledWith(APP_ID_KEY, any());
});
it("does not set anonymousAppId", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).not.toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, any());
});
it("does not remove appId", async () => {
await sut.migrate(helper);
expect(helper.set).not.toHaveBeenCalledWith("appId", any());
});
it("does not remove anonymousAppId", async () => {
await sut.migrate(helper);
expect(helper.set).not.toHaveBeenCalledWith("anonymousAppId", any());
});
});
describe("rollback with both Ids", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 33);
sut = new AppIdMigrator(32, 33);
});
it("removes appId", async () => {
await sut.rollback(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(APP_ID_KEY, null);
});
it("sets appId", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("appId", "appId");
});
it("removes anonymousAppId", async () => {
await sut.rollback(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, null);
});
it("sets anonymousAppId", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("anonymousAppId", "anonymousAppId");
});
});
describe("rollback missing both Ids", () => {
beforeEach(() => {
helper = mockMigrationHelper(missingBothJSON(), 33);
sut = new AppIdMigrator(32, 33);
});
it("does not set appId for providers", async () => {
await sut.rollback(helper);
expect(helper.setToGlobal).not.toHaveBeenCalledWith(APP_ID_KEY, any());
});
it("does not set anonymousAppId for providers", async () => {
await sut.rollback(helper);
expect(helper.setToGlobal).not.toHaveBeenCalledWith(ANONYMOUS_APP_ID_KEY, any());
});
it("does not revert appId", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("appId", any());
});
it("does not revert anonymousAppId", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("anonymousAppId", any());
});
});
});

View File

@@ -0,0 +1,46 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
export const APP_ID_STORAGE_KEY = "appId";
export const ANONYMOUS_APP_ID_STORAGE_KEY = "anonymousAppId";
export const APP_ID_KEY: KeyDefinitionLike = {
key: APP_ID_STORAGE_KEY,
stateDefinition: { name: "applicationId" },
};
export const ANONYMOUS_APP_ID_KEY: KeyDefinitionLike = {
key: ANONYMOUS_APP_ID_STORAGE_KEY,
stateDefinition: { name: "applicationId" },
};
export class AppIdMigrator extends Migrator<32, 33> {
async migrate(helper: MigrationHelper): Promise<void> {
const appId = await helper.get<string>(APP_ID_STORAGE_KEY);
const anonymousAppId = await helper.get<string>(ANONYMOUS_APP_ID_STORAGE_KEY);
if (appId != null) {
await helper.setToGlobal(APP_ID_KEY, appId);
await helper.set(APP_ID_STORAGE_KEY, null);
}
if (anonymousAppId != null) {
await helper.setToGlobal(ANONYMOUS_APP_ID_KEY, anonymousAppId);
await helper.set(ANONYMOUS_APP_ID_STORAGE_KEY, null);
}
}
async rollback(helper: MigrationHelper): Promise<void> {
const appId = await helper.getFromGlobal<string>(APP_ID_KEY);
const anonymousAppId = await helper.getFromGlobal<string>(ANONYMOUS_APP_ID_KEY);
if (appId != null) {
await helper.set(APP_ID_STORAGE_KEY, appId);
await helper.setToGlobal(APP_ID_KEY, null);
}
if (anonymousAppId != null) {
await helper.set(ANONYMOUS_APP_ID_STORAGE_KEY, anonymousAppId);
await helper.setToGlobal(ANONYMOUS_APP_ID_KEY, null);
}
}
}

View File

@@ -0,0 +1,255 @@
import { any, MockProxy } from "jest-mock-extended";
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { DomainSettingsMigrator } from "./34-move-domain-settings-to-state-providers";
const mockNeverDomains = { "bitwarden.test": null, locahost: null, "www.example.com": null } as {
[key: string]: null;
};
function exampleJSON() {
return {
global: {
otherStuff: "otherStuff1",
neverDomains: mockNeverDomains,
},
authenticatedAccounts: ["user-1", "user-2", "user-3"],
"user-1": {
settings: {
defaultUriMatch: 3,
settings: {
equivalentDomains: [] as string[][],
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
settings: {
settings: {
equivalentDomains: [["apple.com", "icloud.com"]],
},
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
"user-3": {
settings: {
defaultUriMatch: 1,
otherStuff: "otherStuff6",
},
otherStuff: "otherStuff7",
},
"user-4": {
settings: {
otherStuff: "otherStuff8",
},
otherStuff: "otherStuff9",
},
};
}
function rollbackJSON() {
return {
global_domainSettings_neverDomains: mockNeverDomains,
"user_user-1_domainSettings_defaultUriMatchStrategy": 3,
"user_user-1_domainSettings_equivalentDomains": [] as string[][],
"user_user-2_domainSettings_equivalentDomains": [["apple.com", "icloud.com"]],
"user_user-3_domainSettings_defaultUriMatchStrategy": 1,
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user-1", "user-2", "user-3"],
"user-1": {
settings: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
settings: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
"user-3": {
settings: {
otherStuff: "otherStuff6",
},
otherStuff: "otherStuff7",
},
"user-4": {
settings: {
otherStuff: "otherStuff8",
},
otherStuff: "otherStuff9",
},
};
}
const domainSettingsStateDefinition: {
stateDefinition: StateDefinitionLike;
} = {
stateDefinition: {
name: "domainSettings",
},
};
describe("DomainSettingsMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: DomainSettingsMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 33);
sut = new DomainSettingsMigrator(33, 34);
});
it("should remove global neverDomains and defaultUriMatch and equivalentDomains settings from all accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledTimes(4);
expect(helper.set).toHaveBeenCalledWith("global", {
otherStuff: "otherStuff1",
});
expect(helper.set).toHaveBeenCalledWith("user-1", {
settings: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).toHaveBeenCalledWith("user-1", {
settings: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).toHaveBeenCalledWith("user-2", {
settings: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
});
expect(helper.set).toHaveBeenCalledWith("user-3", {
settings: {
otherStuff: "otherStuff6",
},
otherStuff: "otherStuff7",
});
});
it("should set global neverDomains and defaultUriMatchStrategy and equivalentDomains setting values for each account", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
expect(helper.setToGlobal).toHaveBeenCalledWith(
{ ...domainSettingsStateDefinition, key: "neverDomains" },
mockNeverDomains,
);
expect(helper.setToUser).toHaveBeenCalledTimes(4);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
3,
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
[],
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-2",
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
[["apple.com", "icloud.com"]],
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-3",
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
1,
);
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 34);
sut = new DomainSettingsMigrator(33, 34);
});
it("should null out new values globally and for each account", async () => {
await sut.rollback(helper);
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
expect(helper.setToGlobal).toHaveBeenCalledWith(
{ ...domainSettingsStateDefinition, key: "neverDomains" },
null,
);
expect(helper.setToUser).toHaveBeenCalledTimes(4);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
null,
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
null,
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-2",
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
null,
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-3",
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
null,
);
});
it("should add explicit value back to accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledTimes(4);
expect(helper.set).toHaveBeenCalledWith("global", {
neverDomains: mockNeverDomains,
otherStuff: "otherStuff1",
});
expect(helper.set).toHaveBeenCalledWith("user-1", {
settings: {
defaultUriMatch: 3,
settings: {
equivalentDomains: [] as string[][],
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).toHaveBeenCalledWith("user-2", {
settings: {
settings: {
equivalentDomains: [["apple.com", "icloud.com"]],
},
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
});
expect(helper.set).toHaveBeenCalledWith("user-3", {
settings: {
defaultUriMatch: 1,
otherStuff: "otherStuff6",
},
otherStuff: "otherStuff7",
});
});
it("should not try to restore values to missing accounts", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("user-4", any());
});
});
});

View File

@@ -0,0 +1,167 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
const UriMatchStrategy = {
Domain: 0,
Host: 1,
StartsWith: 2,
Exact: 3,
RegularExpression: 4,
Never: 5,
} as const;
type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
type ExpectedAccountState = {
settings?: {
defaultUriMatch?: UriMatchStrategySetting;
settings?: {
equivalentDomains?: string[][];
};
};
};
type ExpectedGlobalState = {
neverDomains?: { [key: string]: null };
};
const defaultUriMatchStrategyDefinition: KeyDefinitionLike = {
stateDefinition: {
name: "domainSettings",
},
key: "defaultUriMatchStrategy",
};
const equivalentDomainsDefinition: KeyDefinitionLike = {
stateDefinition: {
name: "domainSettings",
},
key: "equivalentDomains",
};
const neverDomainsDefinition: KeyDefinitionLike = {
stateDefinition: {
name: "domainSettings",
},
key: "neverDomains",
};
export class DomainSettingsMigrator extends Migrator<33, 34> {
async migrate(helper: MigrationHelper): Promise<void> {
let updateAccount = false;
// global state ("neverDomains")
const globalState = await helper.get<ExpectedGlobalState>("global");
if (globalState?.neverDomains != null) {
await helper.setToGlobal(neverDomainsDefinition, globalState.neverDomains);
// delete `neverDomains` from state global
delete globalState.neverDomains;
await helper.set<ExpectedGlobalState>("global", globalState);
}
// account state ("defaultUriMatch" and "settings.equivalentDomains")
const accounts = await helper.getAccounts<ExpectedAccountState>();
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
// migrate account state
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
const accountSettings = account?.settings;
if (accountSettings?.defaultUriMatch != undefined) {
await helper.setToUser(
userId,
defaultUriMatchStrategyDefinition,
accountSettings.defaultUriMatch,
);
delete account.settings.defaultUriMatch;
updateAccount = true;
}
if (accountSettings?.settings?.equivalentDomains != undefined) {
await helper.setToUser(
userId,
equivalentDomainsDefinition,
accountSettings.settings.equivalentDomains,
);
delete account.settings.settings.equivalentDomains;
delete account.settings.settings;
updateAccount = true;
}
if (updateAccount) {
// update the state account settings with the migrated values deleted
await helper.set(userId, account);
}
}
}
async rollback(helper: MigrationHelper): Promise<void> {
let updateAccount = false;
// global state ("neverDomains")
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
const neverDomains: { [key: string]: null } =
await helper.getFromGlobal(neverDomainsDefinition);
if (neverDomains != null) {
await helper.set<ExpectedGlobalState>("global", {
...globalState,
neverDomains: neverDomains,
});
// remove the global state provider framework key for `neverDomains`
await helper.setToGlobal(neverDomainsDefinition, null);
}
// account state ("defaultUriMatchStrategy" and "equivalentDomains")
const accounts = await helper.getAccounts<ExpectedAccountState>();
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
// rollback account state
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
let settings = account?.settings || {};
const defaultUriMatchStrategy: UriMatchStrategySetting = await helper.getFromUser(
userId,
defaultUriMatchStrategyDefinition,
);
const equivalentDomains: string[][] = await helper.getFromUser(
userId,
equivalentDomainsDefinition,
);
// update new settings and remove the account state provider framework keys for the rolled back values
if (defaultUriMatchStrategy != null) {
settings = { ...settings, defaultUriMatch: defaultUriMatchStrategy };
await helper.setToUser(userId, defaultUriMatchStrategyDefinition, null);
updateAccount = true;
}
if (equivalentDomains != null) {
settings = { ...settings, settings: { equivalentDomains } };
await helper.setToUser(userId, equivalentDomainsDefinition, null);
updateAccount = true;
}
// commit updated settings to state
if (updateAccount) {
await helper.set(userId, {
...account,
settings,
});
}
}
}
}

View File

@@ -3,7 +3,7 @@ import { MockProxy } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { LocalDataMigrator } from "./33-move-local-data-to-state-provider";
import { LocalDataMigrator } from "./35-move-local-data-to-state-provider";
function exampleJSON() {
return {

View File

@@ -1,5 +1,5 @@
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { UriMatchType } from "../enums";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";
import { Cipher } from "../models/domain/cipher";
@@ -25,7 +25,7 @@ export abstract class CipherService {
getAllDecryptedForUrl: (
url: string,
includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchType,
defaultMatch?: UriMatchStrategySetting,
) => Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
/**

View File

@@ -3,4 +3,3 @@ export * from "./cipher-type";
export * from "./field-type.enum";
export * from "./linked-id-type.enum";
export * from "./secure-note-type.enum";
export * from "./uri-match-type.enum";

View File

@@ -1,8 +0,0 @@
export enum UriMatchType {
Domain = 0,
Host = 1,
StartsWith = 2,
Exact = 3,
RegularExpression = 4,
Never = 5,
}

View File

@@ -1,10 +1,10 @@
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { BaseResponse } from "../../../models/response/base.response";
import { UriMatchType } from "../../enums";
export class LoginUriApi extends BaseResponse {
uri: string;
uriChecksum: string;
match: UriMatchType = null;
match: UriMatchStrategySetting = null;
constructor(data: any = null) {
super(data);

View File

@@ -1,10 +1,10 @@
import { UriMatchType } from "../../enums";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { LoginUriApi } from "../api/login-uri.api";
export class LoginUriData {
uri: string;
uriChecksum: string;
match: UriMatchType = null;
match: UriMatchStrategySetting = null;
constructor(data?: LoginUriApi) {
if (data == null) {

View File

@@ -2,13 +2,14 @@ import { mock } from "jest-mock-extended";
import { Jsonify } from "type-fest";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string";
import { ContainerService } from "../../../platform/services/container.service";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { CipherService } from "../../abstractions/cipher.service";
import { FieldType, SecureNoteType, UriMatchType } from "../../enums";
import { FieldType, SecureNoteType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { CipherData } from "../../models/data/cipher.data";
@@ -76,7 +77,11 @@ describe("Cipher DTO", () => {
key: "EncryptedString",
login: {
uris: [
{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchType.Domain },
{
uri: "EncryptedString",
uriChecksum: "EncryptedString",
match: UriMatchStrategy.Domain,
},
],
username: "EncryptedString",
password: "EncryptedString",

View File

@@ -2,9 +2,9 @@ import { MockProxy, mock } from "jest-mock-extended";
import { Jsonify } from "type-fest";
import { mockEnc, mockFromJson } from "../../../../spec";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string";
import { UriMatchType } from "../../enums";
import { LoginUriData } from "../data/login-uri.data";
import { LoginUri } from "./login-uri";
@@ -16,7 +16,7 @@ describe("LoginUri", () => {
data = {
uri: "encUri",
uriChecksum: "encUriChecksum",
match: UriMatchType.Domain,
match: UriMatchStrategy.Domain,
};
});
@@ -48,7 +48,7 @@ describe("LoginUri", () => {
it("Decrypt", async () => {
const loginUri = new LoginUri();
loginUri.match = UriMatchType.Exact;
loginUri.match = UriMatchStrategy.Exact;
loginUri.uri = mockEnc("uri");
const view = await loginUri.decrypt(null);
@@ -103,13 +103,13 @@ describe("LoginUri", () => {
const actual = LoginUri.fromJSON({
uri: "myUri",
uriChecksum: "myUriChecksum",
match: UriMatchType.Domain,
match: UriMatchStrategy.Domain,
} as Jsonify<LoginUri>);
expect(actual).toEqual({
uri: "myUri_fromJSON",
uriChecksum: "myUriChecksum_fromJSON",
match: UriMatchType.Domain,
match: UriMatchStrategy.Domain,
});
expect(actual).toBeInstanceOf(LoginUri);
});

View File

@@ -1,17 +1,17 @@
import { Jsonify } from "type-fest";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { UriMatchType } from "../../enums";
import { LoginUriData } from "../data/login-uri.data";
import { LoginUriView } from "../view/login-uri.view";
export class LoginUri extends Domain {
uri: EncString;
uriChecksum: EncString | undefined;
match: UriMatchType;
match: UriMatchStrategySetting;
constructor(obj?: LoginUriData) {
super();

View File

@@ -1,8 +1,8 @@
import { MockProxy, mock } from "jest-mock-extended";
import { mockEnc, mockFromJson } from "../../../../spec";
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { UriMatchType } from "../../enums";
import { LoginData } from "../../models/data/login.data";
import { Login } from "../../models/domain/login";
import { LoginUri } from "../../models/domain/login-uri";
@@ -30,7 +30,7 @@ describe("Login DTO", () => {
it("Convert from full LoginData", () => {
const fido2CredentialData = initializeFido2Credential(new Fido2CredentialData());
const data: LoginData = {
uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchType.Domain }],
uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
username: "username",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
@@ -82,7 +82,7 @@ describe("Login DTO", () => {
totp: "encrypted totp",
uris: [
{
match: null as UriMatchType,
match: null as UriMatchStrategySetting,
_uri: "decrypted uri",
_domain: null as string,
_hostname: null as string,
@@ -123,7 +123,7 @@ describe("Login DTO", () => {
it("Converts from LoginData and back", () => {
const data: LoginData = {
uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchType.Domain }],
uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
username: "username",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",

View File

@@ -1,26 +1,26 @@
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
import { UriMatchType } from "../../enums";
import { LoginUriView } from "./login-uri.view";
const testData = [
{
match: UriMatchType.Host,
match: UriMatchStrategy.Host,
uri: "http://example.com/login",
expected: "http://example.com/login",
},
{
match: UriMatchType.Host,
match: UriMatchStrategy.Host,
uri: "bitwarden.com",
expected: "http://bitwarden.com",
},
{
match: UriMatchType.Host,
match: UriMatchStrategy.Host,
uri: "bitwarden.de",
expected: "http://bitwarden.de",
},
{
match: UriMatchType.Host,
match: UriMatchStrategy.Host,
uri: "bitwarden.br",
expected: "http://bitwarden.br",
},
@@ -41,7 +41,7 @@ const exampleUris = {
describe("LoginUriView", () => {
it("isWebsite() given an invalid domain should return false", async () => {
const uri = new LoginUriView();
Object.assign(uri, { match: UriMatchType.Host, uri: "bit!:_&ward.com" });
Object.assign(uri, { match: UriMatchStrategy.Host, uri: "bit!:_&ward.com" });
expect(uri.isWebsite).toBe(false);
});
@@ -67,32 +67,32 @@ describe("LoginUriView", () => {
it(`canLaunch should return false when MatchDetection is set to Regex`, async () => {
const uri = new LoginUriView();
Object.assign(uri, { match: UriMatchType.RegularExpression, uri: "bitwarden.com" });
Object.assign(uri, { match: UriMatchStrategy.RegularExpression, uri: "bitwarden.com" });
expect(uri.canLaunch).toBe(false);
});
it(`canLaunch() should return false when the given protocol does not match CanLaunchWhiteList`, async () => {
const uri = new LoginUriView();
Object.assign(uri, { match: UriMatchType.Host, uri: "someprotocol://bitwarden.com" });
Object.assign(uri, { match: UriMatchStrategy.Host, uri: "someprotocol://bitwarden.com" });
expect(uri.canLaunch).toBe(false);
});
describe("uri matching", () => {
describe("using domain matching", () => {
it("matches the same domain", () => {
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.Domain, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.subdomain, exampleUris.noEquivalentDomains());
expect(actual).toBe(true);
});
it("matches equivalent domains", () => {
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.Domain, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.differentDomain, exampleUris.equivalentDomains());
expect(actual).toBe(true);
});
it("does not match a different domain", () => {
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.Domain, exampleUris.standard);
const actual = uri.matchesUri(
exampleUris.differentDomain,
exampleUris.noEquivalentDomains(),
@@ -103,7 +103,7 @@ describe("LoginUriView", () => {
// Actual integration test with the real blacklist, not ideal
it("does not match domains that are blacklisted", () => {
const googleEquivalentDomains = new Set(["google.com", "script.google.com"]);
const uri = uriFactory(UriMatchType.Domain, "google.com");
const uri = uriFactory(UriMatchStrategy.Domain, "google.com");
const actual = uri.matchesUri("script.google.com", googleEquivalentDomains);
@@ -113,13 +113,13 @@ describe("LoginUriView", () => {
describe("using host matching", () => {
it("matches the same host", () => {
const uri = uriFactory(UriMatchType.Host, Utils.getHost(exampleUris.standard));
const uri = uriFactory(UriMatchStrategy.Host, Utils.getHost(exampleUris.standard));
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
expect(actual).toBe(true);
});
it("does not match a different host", () => {
const uri = uriFactory(UriMatchType.Host, Utils.getHost(exampleUris.differentDomain));
const uri = uriFactory(UriMatchStrategy.Host, Utils.getHost(exampleUris.differentDomain));
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
expect(actual).toBe(false);
});
@@ -127,13 +127,13 @@ describe("LoginUriView", () => {
describe("using exact matching", () => {
it("matches if both uris are the same", () => {
const uri = uriFactory(UriMatchType.Exact, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.Exact, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
expect(actual).toBe(true);
});
it("does not match if the uris are different", () => {
const uri = uriFactory(UriMatchType.Exact, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.Exact, exampleUris.standard);
const actual = uri.matchesUri(
exampleUris.standard + "#",
exampleUris.noEquivalentDomains(),
@@ -144,7 +144,7 @@ describe("LoginUriView", () => {
describe("using startsWith matching", () => {
it("matches if the target URI starts with the saved URI", () => {
const uri = uriFactory(UriMatchType.StartsWith, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.StartsWith, exampleUris.standard);
const actual = uri.matchesUri(
exampleUris.standard + "#bookmark",
exampleUris.noEquivalentDomains(),
@@ -153,7 +153,7 @@ describe("LoginUriView", () => {
});
it("does not match if the start of the uri is not the same", () => {
const uri = uriFactory(UriMatchType.StartsWith, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.StartsWith, exampleUris.standard);
const actual = uri.matchesUri(
exampleUris.standard.slice(1),
exampleUris.noEquivalentDomains(),
@@ -164,13 +164,13 @@ describe("LoginUriView", () => {
describe("using regular expression matching", () => {
it("matches if the regular expression matches", () => {
const uri = uriFactory(UriMatchType.RegularExpression, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.RegularExpression, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.standardRegex, exampleUris.noEquivalentDomains());
expect(actual).toBe(false);
});
it("does not match if the regular expression does not match", () => {
const uri = uriFactory(UriMatchType.RegularExpression, exampleUris.standardNotMatching);
const uri = uriFactory(UriMatchStrategy.RegularExpression, exampleUris.standardNotMatching);
const actual = uri.matchesUri(exampleUris.standardRegex, exampleUris.noEquivalentDomains());
expect(actual).toBe(false);
});
@@ -178,7 +178,7 @@ describe("LoginUriView", () => {
describe("using never matching", () => {
it("does not match even if uris are identical", () => {
const uri = uriFactory(UriMatchType.Never, exampleUris.standard);
const uri = uriFactory(UriMatchStrategy.Never, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
expect(actual).toBe(false);
});
@@ -186,7 +186,7 @@ describe("LoginUriView", () => {
});
});
function uriFactory(match: UriMatchType, uri: string) {
function uriFactory(match: UriMatchStrategySetting, uri: string) {
const loginUri = new LoginUriView();
loginUri.match = match;
loginUri.uri = uri;

View File

@@ -1,13 +1,13 @@
import { Jsonify } from "type-fest";
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { View } from "../../../models/view/view";
import { SafeUrls } from "../../../platform/misc/safe-urls";
import { Utils } from "../../../platform/misc/utils";
import { UriMatchType } from "../../enums";
import { LoginUri } from "../domain/login-uri";
export class LoginUriView implements View {
match: UriMatchType = null;
match: UriMatchStrategySetting = null;
private _uri: string = null;
private _domain: string = null;
@@ -44,7 +44,7 @@ export class LoginUriView implements View {
}
get hostname(): string {
if (this.match === UriMatchType.RegularExpression) {
if (this.match === UriMatchStrategy.RegularExpression) {
return null;
}
if (this._hostname == null && this.uri != null) {
@@ -58,7 +58,7 @@ export class LoginUriView implements View {
}
get host(): string {
if (this.match === UriMatchType.RegularExpression) {
if (this.match === UriMatchStrategy.RegularExpression) {
return null;
}
if (this._host == null && this.uri != null) {
@@ -92,7 +92,7 @@ export class LoginUriView implements View {
if (this._canLaunch != null) {
return this._canLaunch;
}
if (this.uri != null && this.match !== UriMatchType.RegularExpression) {
if (this.uri != null && this.match !== UriMatchStrategy.RegularExpression) {
this._canLaunch = SafeUrls.canLaunch(this.launchUri);
} else {
this._canLaunch = false;
@@ -113,30 +113,30 @@ export class LoginUriView implements View {
matchesUri(
targetUri: string,
equivalentDomains: Set<string>,
defaultUriMatch: UriMatchType = null,
defaultUriMatch: UriMatchStrategySetting = null,
): boolean {
if (!this.uri || !targetUri) {
return false;
}
let matchType = this.match ?? defaultUriMatch;
matchType ??= UriMatchType.Domain;
matchType ??= UriMatchStrategy.Domain;
const targetDomain = Utils.getDomain(targetUri);
const matchDomains = equivalentDomains.add(targetDomain);
switch (matchType) {
case UriMatchType.Domain:
case UriMatchStrategy.Domain:
return this.matchesDomain(targetUri, matchDomains);
case UriMatchType.Host: {
case UriMatchStrategy.Host: {
const urlHost = Utils.getHost(targetUri);
return urlHost != null && urlHost === Utils.getHost(this.uri);
}
case UriMatchType.Exact:
case UriMatchStrategy.Exact:
return targetUri === this.uri;
case UriMatchType.StartsWith:
case UriMatchStrategy.StartsWith:
return targetUri.startsWith(this.uri);
case UriMatchType.RegularExpression:
case UriMatchStrategy.RegularExpression:
try {
const regex = new RegExp(this.uri, "i");
return regex.test(targetUri);
@@ -144,7 +144,7 @@ export class LoginUriView implements View {
// Invalid regex
return false;
}
case UriMatchType.Never:
case UriMatchStrategy.Never:
return false;
default:
break;

View File

@@ -1,6 +1,7 @@
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
import { DeepJsonify } from "../../../types/deep-jsonify";
import { LoginLinkedId as LinkedId, UriMatchType } from "../../enums";
import { LoginLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
import { Login } from "../domain/login";
@@ -71,7 +72,7 @@ export class LoginView extends ItemView {
matchesUri(
targetUri: string,
equivalentDomains: Set<string>,
defaultUriMatch: UriMatchType = null,
defaultUriMatch: UriMatchStrategySetting = null,
): boolean {
if (this.uris == null) {
return false;

View File

@@ -6,8 +6,9 @@ import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { makeStaticByteArray } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service";
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
@@ -21,7 +22,7 @@ import { ContainerService } from "../../platform/services/container.service";
import { UserId } from "../../types/guid";
import { CipherKey, OrgKey } from "../../types/key";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { UriMatchType, FieldType } from "../enums";
import { FieldType } from "../enums";
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";
@@ -57,7 +58,9 @@ const cipherData: CipherData = {
key: "EncKey",
reprompt: CipherRepromptType.None,
login: {
uris: [{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchType.Domain }],
uris: [
{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchStrategy.Domain },
],
username: "EncryptedString",
password: "EncryptedString",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
@@ -105,7 +108,7 @@ describe("Cipher Service", () => {
const cryptoService = mock<CryptoService>();
const stateService = mock<StateService>();
const autofillSettingsService = mock<AutofillSettingsService>();
const settingsService = mock<SettingsService>();
const domainSettingsService = mock<DomainSettingsService>();
const apiService = mock<ApiService>();
const cipherFileUploadService = mock<CipherFileUploadService>();
const i18nService = mock<I18nService>();
@@ -126,7 +129,7 @@ describe("Cipher Service", () => {
cipherService = new CipherService(
cryptoService,
settingsService,
domainSettingsService,
apiService,
i18nService,
searchService,
@@ -286,7 +289,7 @@ describe("Cipher Service", () => {
it("should add a uri hash to login uris", async () => {
encryptService.hash.mockImplementation((value) => Promise.resolve(`${value} hash`));
cipherView.login.uris = [
{ uri: "uri", match: UriMatchType.RegularExpression } as LoginUriView,
{ uri: "uri", match: UriMatchStrategy.RegularExpression } as LoginUriView,
];
const domain = await cipherService.encrypt(cipherView);
@@ -295,7 +298,7 @@ describe("Cipher Service", () => {
{
uri: new EncString("uri has been encrypted"),
uriChecksum: new EncString("uri hash has been encrypted"),
match: UriMatchType.RegularExpression,
match: UriMatchStrategy.RegularExpression,
},
]);
});

View File

@@ -4,8 +4,9 @@ import { Jsonify } from "type-fest";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service";
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { ErrorResponse } from "../../models/response/error.response";
import { ListResponse } from "../../models/response/list.response";
import { View } from "../../models/view/view";
@@ -32,7 +33,7 @@ import { CipherId } from "../../types/guid";
import { UserKey, OrgKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType, UriMatchType } from "../enums";
import { FieldType } from "../enums";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";
import { LocalData } from "../models/data/local.data";
@@ -94,7 +95,7 @@ export class CipherService implements CipherServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private settingsService: SettingsService,
private domainSettingsService: DomainSettingsService,
private apiService: ApiService,
private i18nService: I18nService,
private searchService: SearchService,
@@ -410,15 +411,17 @@ export class CipherService implements CipherServiceAbstraction {
async getAllDecryptedForUrl(
url: string,
includeOtherTypes?: CipherType[],
defaultMatch: UriMatchType = null,
defaultMatch: UriMatchStrategySetting = null,
): Promise<CipherView[]> {
if (url == null && includeOtherTypes == null) {
return Promise.resolve([]);
}
const equivalentDomains = this.settingsService.getEquivalentDomains(url);
const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(url),
);
const ciphers = await this.getAllDecrypted();
defaultMatch ??= await this.stateService.getDefaultUriMatch();
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
return ciphers.filter((cipher) => {
const cipherIsLogin = cipher.type === CipherType.Login && cipher.login !== null;
@@ -564,12 +567,12 @@ export class CipherService implements CipherServiceAbstraction {
return;
}
let domains = await this.stateService.getNeverDomains();
let domains = await firstValueFrom(this.domainSettingsService.neverDomains$);
if (!domains) {
domains = {};
}
domains[domain] = null;
await this.stateService.setNeverDomains(domains);
await this.domainSettingsService.setNeverDomains(domains);
}
async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise<any> {

View File

@@ -3,6 +3,7 @@ import { of } from "rxjs";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
@@ -34,6 +35,7 @@ describe("FidoAuthenticatorService", () => {
let authService!: MockProxy<AuthService>;
let stateService!: MockProxy<StateService>;
let vaultSettingsService: MockProxy<VaultSettingsService>;
let domainSettingsService: MockProxy<DomainSettingsService>;
let client!: Fido2ClientService;
let tab!: chrome.tabs.Tab;
@@ -43,6 +45,7 @@ describe("FidoAuthenticatorService", () => {
authService = mock<AuthService>();
stateService = mock<StateService>();
vaultSettingsService = mock<VaultSettingsService>();
domainSettingsService = mock<DomainSettingsService>();
client = new Fido2ClientService(
authenticator,
@@ -50,9 +53,11 @@ describe("FidoAuthenticatorService", () => {
authService,
stateService,
vaultSettingsService,
domainSettingsService,
);
configService.serverConfig$ = of({ environment: { vault: VaultUrl } } as any);
vaultSettingsService.enablePasskeys$ = of(true);
domainSettingsService.neverDomains$ = of({});
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
});
@@ -130,7 +135,7 @@ describe("FidoAuthenticatorService", () => {
origin: "https://bitwarden.com",
rp: { id: "bitwarden.com", name: "Bitwarden" },
});
stateService.getNeverDomains.mockResolvedValue({ "bitwarden.com": null });
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.createCredential(params, tab);
@@ -376,7 +381,8 @@ describe("FidoAuthenticatorService", () => {
const params = createParams({
origin: "https://bitwarden.com",
});
stateService.getNeverDomains.mockResolvedValue({ "bitwarden.com": null });
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.assertCredential(params, tab);

View File

@@ -3,6 +3,7 @@ import { parse } from "tldts";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service";
@@ -44,6 +45,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
private authService: AuthService,
private stateService: StateService,
private vaultSettingsService: VaultSettingsService,
private domainSettingsService: DomainSettingsService,
private logService?: LogService,
) {}
@@ -52,7 +54,8 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
const isUserLoggedIn =
(await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut;
const neverDomains = await this.stateService.getNeverDomains();
const neverDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const isExcludedDomain = neverDomains != null && hostname in neverDomains;
const serverConfig = await firstValueFrom(this.configService.serverConfig$);

View File

@@ -1,5 +1,4 @@
import { ApiService } from "../../../abstractions/api.service";
import { SettingsService } from "../../../abstractions/settings.service";
import { InternalOrganizationServiceAbstraction } 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";
@@ -10,6 +9,7 @@ import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { DomainsResponse } from "../../../models/response/domains.response";
import {
SyncCipherNotification,
@@ -44,7 +44,7 @@ export class SyncService implements SyncServiceAbstraction {
constructor(
private apiService: ApiService,
private settingsService: SettingsService,
private domainSettingsService: DomainSettingsService,
private folderService: InternalFolderService,
private cipherService: CipherService,
private cryptoService: CryptoService,
@@ -457,7 +457,7 @@ export class SyncService implements SyncServiceAbstraction {
});
}
return this.settingsService.setEquivalentDomains(eqDomains);
return this.domainSettingsService.setEquivalentDomains(eqDomains);
}
private async syncPolicies(response: PolicyResponse[]) {