mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-04 13:44:00 +01:00
Merge branch 'main' into vault/pm-5273
This commit is contained in:
commit
b17239595d
@ -9,7 +9,10 @@ module.exports = {
|
|||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
preset: "jest-preset-angular",
|
preset: "jest-preset-angular",
|
||||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(
|
||||||
prefix: "<rootDir>/",
|
{ "@bitwarden/common/spec": ["../../libs/common/spec"], ...(compilerOptions?.paths ?? {}) },
|
||||||
}),
|
{
|
||||||
|
prefix: "<rootDir>/",
|
||||||
|
},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
@ -2708,11 +2708,11 @@
|
|||||||
"launchDuoAndFollowStepsToFinishLoggingIn": {
|
"launchDuoAndFollowStepsToFinishLoggingIn": {
|
||||||
"message": "Launch Duo and follow the steps to finish logging in."
|
"message": "Launch Duo and follow the steps to finish logging in."
|
||||||
},
|
},
|
||||||
"duoRequiredByOrgForAccount": {
|
"duoRequiredForAccount": {
|
||||||
"message": "Duo two-step login is required for your account."
|
"message": "Duo two-step login is required for your account."
|
||||||
},
|
},
|
||||||
"openExtensionInNewWindowToCompleteLogin": {
|
"popoutTheExtensionToCompleteLogin": {
|
||||||
"message": "Open the extension in a new window to complete login"
|
"message": "Popout the extension to complete login."
|
||||||
},
|
},
|
||||||
"popoutExtension": {
|
"popoutExtension": {
|
||||||
"message": "Popout extension"
|
"message": "Popout extension"
|
||||||
|
@ -112,15 +112,12 @@
|
|||||||
<!-- Duo -->
|
<!-- Duo -->
|
||||||
<ng-container *ngIf="isDuoProvider">
|
<ng-container *ngIf="isDuoProvider">
|
||||||
<div *ngIf="duoFrameless" class="tw-my-4">
|
<div *ngIf="duoFrameless" class="tw-my-4">
|
||||||
<p
|
<p class="tw-mb-0 tw-text-center">
|
||||||
*ngIf="selectedProviderType === providerType.OrganizationDuo"
|
{{ "duoRequiredForAccount" | i18n }}
|
||||||
class="tw-mb-0 tw-text-center"
|
|
||||||
>
|
|
||||||
{{ "duoRequiredByOrgForAccount" | i18n }}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="tw-text-center" *ngIf="!inPopout">
|
<p class="tw-text-center" *ngIf="!inPopout">
|
||||||
{{ "openExtensionInNewWindowToCompleteLogin" | i18n }}
|
{{ "popoutTheExtensionToCompleteLogin" | i18n }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ng-container *ngIf="inPopout">
|
<ng-container *ngIf="inPopout">
|
||||||
|
@ -208,7 +208,19 @@ describe("AutofillInit", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the isCurrentlyFilling properties of the overlay and focus the recent field after filling", async () => {
|
it("removes the overlay when filling the form", async () => {
|
||||||
|
const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay");
|
||||||
|
sendExtensionRuntimeMessage({
|
||||||
|
command: "fillForm",
|
||||||
|
fillScript,
|
||||||
|
pageDetailsUrl: window.location.href,
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(blurAndRemoveOverlaySpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling");
|
jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling");
|
||||||
jest
|
jest
|
||||||
@ -228,9 +240,6 @@ describe("AutofillInit", () => {
|
|||||||
fillScript,
|
fillScript,
|
||||||
);
|
);
|
||||||
expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false);
|
expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false);
|
||||||
expect(
|
|
||||||
autofillInit["autofillOverlayContentService"].focusMostRecentOverlayField,
|
|
||||||
).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => {
|
it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => {
|
||||||
|
@ -98,6 +98,7 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.blurAndRemoveOverlay();
|
||||||
this.updateOverlayIsCurrentlyFilling(true);
|
this.updateOverlayIsCurrentlyFilling(true);
|
||||||
await this.insertAutofillContentService.fillForm(fillScript);
|
await this.insertAutofillContentService.fillForm(fillScript);
|
||||||
|
|
||||||
@ -105,10 +106,7 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250);
|
||||||
this.updateOverlayIsCurrentlyFilling(false);
|
|
||||||
this.autofillOverlayContentService.focusMostRecentOverlayField();
|
|
||||||
}, 250);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -757,7 +757,7 @@ export default class MainBackground {
|
|||||||
this.platformUtilsService.isSafari() ||
|
this.platformUtilsService.isSafari() ||
|
||||||
this.platformUtilsService.isFirefox() ||
|
this.platformUtilsService.isFirefox() ||
|
||||||
this.platformUtilsService.isOpera();
|
this.platformUtilsService.isOpera();
|
||||||
BrowserApi.reloadExtension(forceWindowReload ? window : null);
|
BrowserApi.reloadExtension(forceWindowReload ? self : null);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -185,6 +185,31 @@ describe("BrowserApi", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("reloadExtension", () => {
|
||||||
|
it("reloads the window location if the passed globalContext is for the window", () => {
|
||||||
|
const windowMock = mock<Window>({
|
||||||
|
location: { reload: jest.fn() },
|
||||||
|
}) as unknown as Window & typeof globalThis;
|
||||||
|
|
||||||
|
BrowserApi.reloadExtension(windowMock);
|
||||||
|
|
||||||
|
expect(windowMock.location.reload).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloads the extension runtime if the passed globalContext is not for the window", () => {
|
||||||
|
const globalMock = mock<typeof globalThis>({}) as any;
|
||||||
|
BrowserApi.reloadExtension(globalMock);
|
||||||
|
|
||||||
|
expect(chrome.runtime.reload).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloads the extension runtime if a null value is passed as the globalContext", () => {
|
||||||
|
BrowserApi.reloadExtension(null);
|
||||||
|
|
||||||
|
expect(chrome.runtime.reload).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("reloadOpenWindows", () => {
|
describe("reloadOpenWindows", () => {
|
||||||
const href = window.location.href;
|
const href = window.location.href;
|
||||||
const reload = window.location.reload;
|
const reload = window.location.reload;
|
||||||
|
@ -381,12 +381,20 @@ export class BrowserApi {
|
|||||||
return chrome.i18n.getUILanguage();
|
return chrome.i18n.getUILanguage();
|
||||||
}
|
}
|
||||||
|
|
||||||
static reloadExtension(win: Window) {
|
/**
|
||||||
if (win != null) {
|
* Handles reloading the extension, either by calling the window location
|
||||||
return (win.location as any).reload(true);
|
* to reload or by calling the extension's runtime to reload.
|
||||||
} else {
|
*
|
||||||
return chrome.runtime.reload();
|
* @param globalContext - The global context to use for the reload.
|
||||||
|
*/
|
||||||
|
static reloadExtension(globalContext: (Window & typeof globalThis) | null) {
|
||||||
|
// The passed globalContext might be a ServiceWorkerGlobalScope, as a result
|
||||||
|
// we need to check if the location object exists before calling reload on it.
|
||||||
|
if (typeof globalContext?.location?.reload === "function") {
|
||||||
|
return (globalContext as any).location.reload(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return chrome.runtime.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
||||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||||
|
import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
@ -29,9 +30,9 @@ export class BrowserCryptoService extends CryptoService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userKey = await this.stateService.getUserKey({ userId: userId });
|
const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey.keyB64)) as UserKey;
|
return userKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import {
|
||||||
@ -11,8 +10,10 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
|
|||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { State } from "@bitwarden/common/platform/models/domain/state";
|
import { State } from "@bitwarden/common/platform/models/domain/state";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
|
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { BrowserComponentState } from "../../models/browserComponentState";
|
import { BrowserComponentState } from "../../models/browserComponentState";
|
||||||
@ -30,12 +31,12 @@ describe("Browser State Service", () => {
|
|||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
|
let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
|
||||||
let useAccountCache: boolean;
|
let useAccountCache: boolean;
|
||||||
let accountService: MockProxy<AccountService>;
|
|
||||||
let environmentService: MockProxy<EnvironmentService>;
|
let environmentService: MockProxy<EnvironmentService>;
|
||||||
let migrationRunner: MockProxy<MigrationRunner>;
|
let migrationRunner: MockProxy<MigrationRunner>;
|
||||||
|
|
||||||
let state: State<GlobalState, Account>;
|
let state: State<GlobalState, Account>;
|
||||||
const userId = "userId";
|
const userId = "userId" as UserId;
|
||||||
|
const accountService = mockAccountServiceWith(userId);
|
||||||
|
|
||||||
let sut: BrowserStateService;
|
let sut: BrowserStateService;
|
||||||
|
|
||||||
@ -44,7 +45,6 @@ describe("Browser State Service", () => {
|
|||||||
diskStorageService = mock();
|
diskStorageService = mock();
|
||||||
logService = mock();
|
logService = mock();
|
||||||
stateFactory = mock();
|
stateFactory = mock();
|
||||||
accountService = mock();
|
|
||||||
environmentService = mock();
|
environmentService = mock();
|
||||||
migrationRunner = mock();
|
migrationRunner = mock();
|
||||||
// turn off account cache for tests
|
// turn off account cache for tests
|
||||||
@ -57,6 +57,10 @@ describe("Browser State Service", () => {
|
|||||||
state.activeUserId = userId;
|
state.activeUserId = userId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe("state methods", () => {
|
describe("state methods", () => {
|
||||||
let memoryStorageService: MockProxy<AbstractMemoryStorageService>;
|
let memoryStorageService: MockProxy<AbstractMemoryStorageService>;
|
||||||
|
|
||||||
|
@ -34,8 +34,6 @@ export class BrowserStateService
|
|||||||
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
|
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
|
||||||
@sessionSync({ initializer: (s: string) => s })
|
@sessionSync({ initializer: (s: string) => s })
|
||||||
protected activeAccountSubject: BehaviorSubject<string>;
|
protected activeAccountSubject: BehaviorSubject<string>;
|
||||||
@sessionSync({ initializer: (b: boolean) => b })
|
|
||||||
protected activeAccountUnlockedSubject: BehaviorSubject<boolean>;
|
|
||||||
|
|
||||||
protected accountDeserializer = Account.fromJSON;
|
protected accountDeserializer = Account.fromJSON;
|
||||||
|
|
||||||
|
@ -73,6 +73,7 @@ import { ConfigService } from "@bitwarden/common/platform/services/config/config
|
|||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
|
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||||
import { DerivedStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
import { DerivedStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
@ -109,6 +110,7 @@ import { BrowserApi } from "../../platform/browser/browser-api";
|
|||||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||||
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
||||||
import { BrowserConfigService } from "../../platform/services/browser-config.service";
|
import { BrowserConfigService } from "../../platform/services/browser-config.service";
|
||||||
|
import { BrowserCryptoService } from "../../platform/services/browser-crypto.service";
|
||||||
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
|
||||||
import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service";
|
import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service";
|
||||||
import { BrowserI18nService } from "../../platform/services/browser-i18n.service";
|
import { BrowserI18nService } from "../../platform/services/browser-i18n.service";
|
||||||
@ -210,7 +212,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
{ provide: CipherService, useFactory: getBgService<CipherService>("cipherService"), deps: [] },
|
{ provide: CipherService, useFactory: getBgService<CipherService>("cipherService"), deps: [] },
|
||||||
{
|
{
|
||||||
provide: CryptoFunctionService,
|
provide: CryptoFunctionService,
|
||||||
useFactory: getBgService<CryptoFunctionService>("cryptoFunctionService"),
|
useFactory: () => new WebCryptoFunctionService(window),
|
||||||
deps: [],
|
deps: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -258,12 +260,36 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: CryptoService,
|
provide: CryptoService,
|
||||||
useFactory: (encryptService: EncryptService) => {
|
useFactory: (
|
||||||
const cryptoService = getBgService<CryptoService>("cryptoService")();
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
encryptService: EncryptService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
logService: LogServiceAbstraction,
|
||||||
|
stateService: StateServiceAbstraction,
|
||||||
|
accountService: AccountServiceAbstraction,
|
||||||
|
stateProvider: StateProvider,
|
||||||
|
) => {
|
||||||
|
const cryptoService = new BrowserCryptoService(
|
||||||
|
cryptoFunctionService,
|
||||||
|
encryptService,
|
||||||
|
platformUtilsService,
|
||||||
|
logService,
|
||||||
|
stateService,
|
||||||
|
accountService,
|
||||||
|
stateProvider,
|
||||||
|
);
|
||||||
new ContainerService(cryptoService, encryptService).attachToGlobal(self);
|
new ContainerService(cryptoService, encryptService).attachToGlobal(self);
|
||||||
return cryptoService;
|
return cryptoService;
|
||||||
},
|
},
|
||||||
deps: [EncryptService],
|
deps: [
|
||||||
|
CryptoFunctionService,
|
||||||
|
EncryptService,
|
||||||
|
PlatformUtilsService,
|
||||||
|
LogServiceAbstraction,
|
||||||
|
StateServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AuthRequestCryptoServiceAbstraction,
|
provide: AuthRequestCryptoServiceAbstraction,
|
||||||
|
@ -30,6 +30,7 @@ const runtime = {
|
|||||||
addListener: jest.fn(),
|
addListener: jest.fn(),
|
||||||
removeListener: jest.fn(),
|
removeListener: jest.fn(),
|
||||||
},
|
},
|
||||||
|
reload: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextMenus = {
|
const contextMenus = {
|
||||||
|
@ -76,7 +76,17 @@
|
|||||||
{{ i.amount | currency: "$" }}
|
{{ i.amount | currency: "$" }}
|
||||||
</td>
|
</td>
|
||||||
<td bitCell class="tw-text-right">
|
<td bitCell class="tw-text-right">
|
||||||
{{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }}
|
<ng-container
|
||||||
|
*ngIf="
|
||||||
|
sub?.customerDiscount?.appliesTo?.includes(i.productId);
|
||||||
|
else calculateElse
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ "freeForOneYear" | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #calculateElse>
|
||||||
|
{{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }}
|
||||||
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -111,6 +111,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
})
|
})
|
||||||
.sort(sortSubscriptionItems);
|
.sort(sortSubscriptionItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.sub?.customerDiscount?.percentOff == 100) {
|
||||||
|
this.lineItems.reverse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKeyResponse = await this.organizationApiService.getApiKeyInformation(
|
const apiKeyResponse = await this.organizationApiService.getApiKeyInformation(
|
||||||
@ -152,6 +156,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
sponsoredSubscriptionItem: lineItem.sponsoredSubscriptionItem,
|
sponsoredSubscriptionItem: lineItem.sponsoredSubscriptionItem,
|
||||||
addonSubscriptionItem: lineItem.addonSubscriptionItem,
|
addonSubscriptionItem: lineItem.addonSubscriptionItem,
|
||||||
productName: lineItem.productName,
|
productName: lineItem.productName,
|
||||||
|
productId: lineItem.productId,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,7 @@
|
|||||||
import { AfterContentInit, Directive, HostListener, Input } from "@angular/core";
|
import { AfterContentInit, Directive, HostListener, Input } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
|
||||||
|
|
||||||
import { SsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
import { SsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
|
||||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[app-link-sso]",
|
selector: "[app-link-sso]",
|
||||||
@ -21,6 +9,8 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge
|
|||||||
export class LinkSsoDirective extends SsoComponent implements AfterContentInit {
|
export class LinkSsoDirective extends SsoComponent implements AfterContentInit {
|
||||||
@Input() organization: Organization;
|
@Input() organization: Organization;
|
||||||
returnUri = "/settings/organizations";
|
returnUri = "/settings/organizations";
|
||||||
|
redirectUri = window.location.origin + "/sso-connector.html";
|
||||||
|
clientId = "web";
|
||||||
|
|
||||||
@HostListener("click", ["$event"])
|
@HostListener("click", ["$event"])
|
||||||
async onClick($event: MouseEvent) {
|
async onClick($event: MouseEvent) {
|
||||||
@ -28,42 +18,6 @@ export class LinkSsoDirective extends SsoComponent implements AfterContentInit {
|
|||||||
await this.submit(this.returnUri, true);
|
await this.submit(this.returnUri, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
|
||||||
ssoLoginService: SsoLoginServiceAbstraction,
|
|
||||||
platformUtilsService: PlatformUtilsService,
|
|
||||||
i18nService: I18nService,
|
|
||||||
apiService: ApiService,
|
|
||||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
|
||||||
router: Router,
|
|
||||||
route: ActivatedRoute,
|
|
||||||
cryptoFunctionService: CryptoFunctionService,
|
|
||||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
|
||||||
stateService: StateService,
|
|
||||||
environmentService: EnvironmentService,
|
|
||||||
logService: LogService,
|
|
||||||
configService: ConfigServiceAbstraction,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
ssoLoginService,
|
|
||||||
loginStrategyService,
|
|
||||||
router,
|
|
||||||
i18nService,
|
|
||||||
route,
|
|
||||||
stateService,
|
|
||||||
platformUtilsService,
|
|
||||||
apiService,
|
|
||||||
cryptoFunctionService,
|
|
||||||
environmentService,
|
|
||||||
passwordGenerationService,
|
|
||||||
logService,
|
|
||||||
configService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.returnUri = "/settings/organizations";
|
|
||||||
this.redirectUri = window.location.origin + "/sso-connector.html";
|
|
||||||
this.clientId = "web";
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngAfterContentInit() {
|
async ngAfterContentInit() {
|
||||||
this.identifier = this.organization.identifier;
|
this.identifier = this.organization.identifier;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
|
|||||||
|
|
||||||
import { SharedModule } from "../../shared";
|
import { SharedModule } from "../../shared";
|
||||||
|
|
||||||
const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="140" height="121" fill="none">
|
const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="10 -10 120 140" fill="none">
|
||||||
<rect class="tw-stroke-secondary-500" width="134" height="86" x="3" y="31.485" stroke-width="6" rx="11"/>
|
<rect class="tw-stroke-secondary-500" width="134" height="86" x="3" y="31.485" stroke-width="6" rx="11"/>
|
||||||
<path class="tw-fill-secondary-500" d="M123.987 20.15H14.779a3.114 3.114 0 0 1-2.083-.95 3.036 3.036 0 0 1 0-4.208 3.125 3.125 0 0 1 2.083-.951h109.208c.792.043 1.536.38 2.083.95a3.035 3.035 0 0 1 0 4.208 3.115 3.115 0 0 1-2.083.95Zm-6.649-14.041h-95.91a3.114 3.114 0 0 1-2.082-.95 3.036 3.036 0 0 1-.848-2.105c0-.782.306-1.538.848-2.104A3.125 3.125 0 0 1 21.43 0h95.909c.791.043 1.535.38 2.082.95.547.57.849 1.322.849 2.104a3.05 3.05 0 0 1-.849 2.104 3.115 3.115 0 0 1-2.082.95ZM95.132 74.407A42.317 42.317 0 0 0 83.59 65.43l8.799-8.657a1.59 1.59 0 0 0 .004-2.27 1.641 1.641 0 0 0-2.298-.004l-9.64 9.479a28.017 28.017 0 0 0-10.483-2.13c-14.323 0-24.814 12.342-25.298 12.89a2.431 2.431 0 0 0-.675 1.64c-.01.612.215 1.203.626 1.66a43.981 43.981 0 0 0 11.873 9.485l-8.806 8.658a1.601 1.601 0 0 0-.499 1.138 1.602 1.602 0 0 0 1.008 1.5 1.651 1.651 0 0 0 1.255-.009c.199-.085.379-.205.528-.359l9.634-9.443a27.16 27.16 0 0 0 10.359 2.158c14.323 0 24.753-12.086 25.23-12.63a2.983 2.983 0 0 0-.078-4.128h.002ZM49.204 77.82a1.82 1.82 0 0 1-.43-.6 1.767 1.767 0 0 1-.152-.72 1.778 1.778 0 0 1 .582-1.32c3.857-3.564 11.782-9.686 20.77-9.676 2.564.037 5.105.508 7.508 1.395l-3.291 3.235a7.793 7.793 0 0 0-5.02-1.226 7.746 7.746 0 0 0-4.676 2.18 7.528 7.528 0 0 0-1 9.563l-4.199 4.143a43.135 43.135 0 0 1-10.092-6.974Zm26.059-1.318a5.19 5.19 0 0 1-1.557 3.68 5.326 5.326 0 0 1-3.733 1.521c-.82-.005-1.63-.2-2.359-.57l7.067-6.952c.377.718.575 1.513.582 2.321Zm-10.58 0a5.136 5.136 0 0 1 .673-2.555 5.204 5.204 0 0 1 1.862-1.897 5.302 5.302 0 0 1 5.172-.146l-7.096 6.977a5.06 5.06 0 0 1-.61-2.379Zm26.053 1.331c-3.857 3.56-11.779 9.677-20.763 9.677a22.723 22.723 0 0 1-7.454-1.369l3.292-3.226a7.793 7.793 0 0 0 4.995 1.192 7.734 7.734 0 0 0 4.642-2.176 7.524 7.524 0 0 0 1.033-9.506l4.224-4.168a43.258 43.258 0 0 1 10.02 6.945 1.788 1.788 0 0 1 .585 1.313 1.788 1.788 0 0 1-.577 1.318h.003Z"/>
|
<path class="tw-fill-secondary-500" d="M123.987 20.15H14.779a3.114 3.114 0 0 1-2.083-.95 3.036 3.036 0 0 1 0-4.208 3.125 3.125 0 0 1 2.083-.951h109.208c.792.043 1.536.38 2.083.95a3.035 3.035 0 0 1 0 4.208 3.115 3.115 0 0 1-2.083.95Zm-6.649-14.041h-95.91a3.114 3.114 0 0 1-2.082-.95 3.036 3.036 0 0 1-.848-2.105c0-.782.306-1.538.848-2.104A3.125 3.125 0 0 1 21.43 0h95.909c.791.043 1.535.38 2.082.95.547.57.849 1.322.849 2.104a3.05 3.05 0 0 1-.849 2.104 3.115 3.115 0 0 1-2.082.95ZM95.132 74.407A42.317 42.317 0 0 0 83.59 65.43l8.799-8.657a1.59 1.59 0 0 0 .004-2.27 1.641 1.641 0 0 0-2.298-.004l-9.64 9.479a28.017 28.017 0 0 0-10.483-2.13c-14.323 0-24.814 12.342-25.298 12.89a2.431 2.431 0 0 0-.675 1.64c-.01.612.215 1.203.626 1.66a43.981 43.981 0 0 0 11.873 9.485l-8.806 8.658a1.601 1.601 0 0 0-.499 1.138 1.602 1.602 0 0 0 1.008 1.5 1.651 1.651 0 0 0 1.255-.009c.199-.085.379-.205.528-.359l9.634-9.443a27.16 27.16 0 0 0 10.359 2.158c14.323 0 24.753-12.086 25.23-12.63a2.983 2.983 0 0 0-.078-4.128h.002ZM49.204 77.82a1.82 1.82 0 0 1-.43-.6 1.767 1.767 0 0 1-.152-.72 1.778 1.778 0 0 1 .582-1.32c3.857-3.564 11.782-9.686 20.77-9.676 2.564.037 5.105.508 7.508 1.395l-3.291 3.235a7.793 7.793 0 0 0-5.02-1.226 7.746 7.746 0 0 0-4.676 2.18 7.528 7.528 0 0 0-1 9.563l-4.199 4.143a43.135 43.135 0 0 1-10.092-6.974Zm26.059-1.318a5.19 5.19 0 0 1-1.557 3.68 5.326 5.326 0 0 1-3.733 1.521c-.82-.005-1.63-.2-2.359-.57l7.067-6.952c.377.718.575 1.513.582 2.321Zm-10.58 0a5.136 5.136 0 0 1 .673-2.555 5.204 5.204 0 0 1 1.862-1.897 5.302 5.302 0 0 1 5.172-.146l-7.096 6.977a5.06 5.06 0 0 1-.61-2.379Zm26.053 1.331c-3.857 3.56-11.779 9.677-20.763 9.677a22.723 22.723 0 0 1-7.454-1.369l3.292-3.226a7.793 7.793 0 0 0 4.995 1.192 7.734 7.734 0 0 0 4.642-2.176 7.524 7.524 0 0 0 1.033-9.506l4.224-4.168a43.258 43.258 0 0 1 10.02 6.945 1.788 1.788 0 0 1 .585 1.313 1.788 1.788 0 0 1-.577 1.318h.003Z"/>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
|
@ -7584,5 +7584,8 @@
|
|||||||
"tooExpensive": {
|
"tooExpensive": {
|
||||||
"message": "Too expensive",
|
"message": "Too expensive",
|
||||||
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||||
|
},
|
||||||
|
"freeForOneYear": {
|
||||||
|
"message": "Free for 1 year"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,10 @@ export class FakeAccountService implements AccountService {
|
|||||||
await this.mock.setAccountStatus(userId, status);
|
await this.mock.setAccountStatus(userId, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void> {
|
||||||
|
await this.mock.setMaxAccountStatus(userId, maxStatus);
|
||||||
|
}
|
||||||
|
|
||||||
async switchAccount(userId: UserId): Promise<void> {
|
async switchAccount(userId: UserId): Promise<void> {
|
||||||
await this.mock.switchAccount(userId);
|
await this.mock.switchAccount(userId);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,9 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FakeStateProvider implements StateProvider {
|
export class FakeStateProvider implements StateProvider {
|
||||||
|
mock = mock<StateProvider>();
|
||||||
getUserState$<T>(keyDefinition: KeyDefinition<T>, userId?: UserId): Observable<T> {
|
getUserState$<T>(keyDefinition: KeyDefinition<T>, userId?: UserId): Observable<T> {
|
||||||
|
this.mock.getUserState$(keyDefinition, userId);
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return this.getUser<T>(userId, keyDefinition).state$;
|
return this.getUser<T>(userId, keyDefinition).state$;
|
||||||
}
|
}
|
||||||
@ -152,6 +154,7 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
value: T,
|
value: T,
|
||||||
userId?: UserId,
|
userId?: UserId,
|
||||||
): Promise<[UserId, T]> {
|
): Promise<[UserId, T]> {
|
||||||
|
await this.mock.setUserState(keyDefinition, value, userId);
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return [userId, await this.getUser(userId, keyDefinition).update(() => value)];
|
return [userId, await this.getUser(userId, keyDefinition).update(() => value)];
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,6 +47,17 @@ export abstract class AccountService {
|
|||||||
* @param status
|
* @param status
|
||||||
*/
|
*/
|
||||||
abstract setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void>;
|
abstract setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Updates the `accounts$` observable with the new account status if the current status is higher than the `maxStatus`.
|
||||||
|
*
|
||||||
|
* This method only downgrades status to the maximum value sent in, it will not increase authentication status.
|
||||||
|
*
|
||||||
|
* @example An account is transitioning from unlocked to logged out. If callbacks that set the status to locked occur
|
||||||
|
* after it is updated to logged out, the account will be in the incorrect state.
|
||||||
|
* @param userId The user id of the account to be updated.
|
||||||
|
* @param maxStatus The new status of the account.
|
||||||
|
*/
|
||||||
|
abstract setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Updates the `activeAccount$` observable with the new active account.
|
* Updates the `activeAccount$` observable with the new active account.
|
||||||
* @param userId
|
* @param userId
|
||||||
|
@ -207,4 +207,26 @@ describe("accountService", () => {
|
|||||||
expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist");
|
expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("setMaxAccountStatus", () => {
|
||||||
|
it("should update the account", async () => {
|
||||||
|
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
||||||
|
await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
|
||||||
|
const currentState = await firstValueFrom(accountsState.state$);
|
||||||
|
|
||||||
|
expect(currentState).toEqual({
|
||||||
|
[userId]: userInfo(AuthenticationStatus.Locked),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not update if the new max status is higher than the current", async () => {
|
||||||
|
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.LoggedOut) });
|
||||||
|
await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
|
||||||
|
const currentState = await firstValueFrom(accountsState.state$);
|
||||||
|
|
||||||
|
expect(currentState).toEqual({
|
||||||
|
[userId]: userInfo(AuthenticationStatus.LoggedOut),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -84,6 +84,24 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void> {
|
||||||
|
await this.accountsState.update(
|
||||||
|
(accounts) => {
|
||||||
|
accounts[userId].status = maxStatus;
|
||||||
|
return accounts;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldUpdate: (accounts) => {
|
||||||
|
if (accounts?.[userId] == null) {
|
||||||
|
throw new Error("Account does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts[userId].status > maxStatus;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async switchAccount(userId: UserId): Promise<void> {
|
async switchAccount(userId: UserId): Promise<void> {
|
||||||
await this.activeAccountIdState.update(
|
await this.activeAccountIdState.update(
|
||||||
(_, accounts) => {
|
(_, accounts) => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
@ -108,7 +110,7 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
|||||||
}
|
}
|
||||||
|
|
||||||
// At this point of rotating their keys, they should still have their old user key in state
|
// At this point of rotating their keys, they should still have their old user key in state
|
||||||
const oldUserKey = await this.stateService.getUserKey();
|
const oldUserKey = await firstValueFrom(this.cryptoService.activeUserKey$);
|
||||||
|
|
||||||
const deviceIdentifier = await this.appIdService.getAppId();
|
const deviceIdentifier = await this.appIdService.getAppId();
|
||||||
const secretVerificationRequest = new SecretVerificationRequest();
|
const secretVerificationRequest = new SecretVerificationRequest();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { matches, mock } from "jest-mock-extended";
|
import { matches, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { DeviceType } from "../../enums";
|
import { DeviceType } from "../../enums";
|
||||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||||
@ -19,6 +20,7 @@ import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trus
|
|||||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||||
|
|
||||||
import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation";
|
import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation";
|
||||||
|
|
||||||
describe("deviceTrustCryptoService", () => {
|
describe("deviceTrustCryptoService", () => {
|
||||||
let deviceTrustCryptoService: DeviceTrustCryptoService;
|
let deviceTrustCryptoService: DeviceTrustCryptoService;
|
||||||
|
|
||||||
@ -495,6 +497,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
const fakeNewUserKeyData = new Uint8Array(64);
|
const fakeNewUserKeyData = new Uint8Array(64);
|
||||||
fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1);
|
fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1);
|
||||||
fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey;
|
fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey;
|
||||||
|
cryptoService.activeUserKey$ = of(fakeNewUserKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does an early exit when the current device is not a trusted device", async () => {
|
it("does an early exit when the current device is not a trusted device", async () => {
|
||||||
@ -521,9 +524,7 @@ describe("deviceTrustCryptoService", () => {
|
|||||||
fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1);
|
fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1);
|
||||||
|
|
||||||
// Mock the retrieval of a user key that differs from the new one passed into the method
|
// Mock the retrieval of a user key that differs from the new one passed into the method
|
||||||
stateService.getUserKey.mockResolvedValue(
|
cryptoService.activeUserKey$ = of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey);
|
||||||
new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
appIdService.getAppId.mockResolvedValue("test_device_identifier");
|
appIdService.getAppId.mockResolvedValue("test_device_identifier");
|
||||||
|
|
||||||
|
@ -12,10 +12,13 @@ import { EncString } from "../models/domain/enc-string";
|
|||||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
export abstract class CryptoService {
|
export abstract class CryptoService {
|
||||||
|
activeUserKey$: Observable<UserKey>;
|
||||||
/**
|
/**
|
||||||
* Sets the provided user key and stores
|
* Sets the provided user key and stores
|
||||||
* any other necessary versions (such as auto, biometrics,
|
* any other necessary versions (such as auto, biometrics,
|
||||||
* or pin)
|
* or pin)
|
||||||
|
*
|
||||||
|
* @throws when key is null. Use {@link clearUserKey} instead
|
||||||
* @param key The user key to set
|
* @param key The user key to set
|
||||||
* @param userId The desired user
|
* @param userId The desired user
|
||||||
*/
|
*/
|
||||||
|
@ -16,7 +16,7 @@ import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
|||||||
import { SendData } from "../../tools/send/models/data/send.data";
|
import { SendData } from "../../tools/send/models/data/send.data";
|
||||||
import { SendView } from "../../tools/send/models/view/send.view";
|
import { SendView } from "../../tools/send/models/view/send.view";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { DeviceKey, MasterKey, UserKey } from "../../types/key";
|
import { DeviceKey, MasterKey } from "../../types/key";
|
||||||
import { UriMatchType } from "../../vault/enums";
|
import { UriMatchType } from "../../vault/enums";
|
||||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||||
import { LocalData } from "../../vault/models/data/local.data";
|
import { LocalData } from "../../vault/models/data/local.data";
|
||||||
@ -50,6 +50,9 @@ export type InitOptions = {
|
|||||||
export abstract class StateService<T extends Account = Account> {
|
export abstract class StateService<T extends Account = Account> {
|
||||||
accounts$: Observable<{ [userId: string]: T }>;
|
accounts$: Observable<{ [userId: string]: T }>;
|
||||||
activeAccount$: Observable<string>;
|
activeAccount$: Observable<string>;
|
||||||
|
/**
|
||||||
|
* @deprecated use accountService.activeAccount$ instead
|
||||||
|
*/
|
||||||
activeAccountUnlocked$: Observable<boolean>;
|
activeAccountUnlocked$: Observable<boolean>;
|
||||||
|
|
||||||
addAccount: (account: T) => Promise<void>;
|
addAccount: (account: T) => Promise<void>;
|
||||||
@ -82,14 +85,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setClearClipboard: (value: number, options?: StorageOptions) => Promise<void>;
|
setClearClipboard: (value: number, options?: StorageOptions) => Promise<void>;
|
||||||
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
/**
|
|
||||||
* gets the user key
|
|
||||||
*/
|
|
||||||
getUserKey: (options?: StorageOptions) => Promise<UserKey>;
|
|
||||||
/**
|
|
||||||
* Sets the user key
|
|
||||||
*/
|
|
||||||
setUserKey: (value: UserKey, options?: StorageOptions) => Promise<void>;
|
|
||||||
/**
|
/**
|
||||||
* Gets the user's master key
|
* Gets the user's master key
|
||||||
*/
|
*/
|
||||||
@ -150,10 +145,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
* @deprecated For migration purposes only, use getUserKeyMasterKey instead
|
* @deprecated For migration purposes only, use getUserKeyMasterKey instead
|
||||||
*/
|
*/
|
||||||
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
|
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
|
||||||
/**
|
|
||||||
* @deprecated For migration purposes only, use setUserKeyMasterKey instead
|
|
||||||
*/
|
|
||||||
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
|
|
||||||
/**
|
/**
|
||||||
* @deprecated For legacy purposes only, use getMasterKey instead
|
* @deprecated For legacy purposes only, use getMasterKey instead
|
||||||
*/
|
*/
|
||||||
|
@ -19,7 +19,7 @@ import { UsernameGeneratorOptions } from "../../../tools/generator/username/user
|
|||||||
import { SendData } from "../../../tools/send/models/data/send.data";
|
import { SendData } from "../../../tools/send/models/data/send.data";
|
||||||
import { SendView } from "../../../tools/send/models/view/send.view";
|
import { SendView } from "../../../tools/send/models/view/send.view";
|
||||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||||
import { MasterKey, UserKey } from "../../../types/key";
|
import { MasterKey } from "../../../types/key";
|
||||||
import { UriMatchType } from "../../../vault/enums";
|
import { UriMatchType } from "../../../vault/enums";
|
||||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||||
import { CipherView } from "../../../vault/models/view/cipher.view";
|
import { CipherView } from "../../../vault/models/view/cipher.view";
|
||||||
@ -113,7 +113,6 @@ export class AccountData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AccountKeys {
|
export class AccountKeys {
|
||||||
userKey?: UserKey;
|
|
||||||
masterKey?: MasterKey;
|
masterKey?: MasterKey;
|
||||||
masterKeyEncryptedUserKey?: string;
|
masterKeyEncryptedUserKey?: string;
|
||||||
deviceKey?: ReturnType<SymmetricCryptoKey["toJSON"]>;
|
deviceKey?: ReturnType<SymmetricCryptoKey["toJSON"]>;
|
||||||
@ -146,7 +145,6 @@ export class AccountKeys {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return Object.assign(new AccountKeys(), obj, {
|
return Object.assign(new AccountKeys(), obj, {
|
||||||
userKey: SymmetricCryptoKey.fromJSON(obj?.userKey),
|
|
||||||
masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey),
|
masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey),
|
||||||
deviceKey: obj?.deviceKey,
|
deviceKey: obj?.deviceKey,
|
||||||
cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey),
|
cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey),
|
||||||
|
@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs";
|
|||||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||||
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
import { CsprngArray } from "../../types/csprng";
|
import { CsprngArray } from "../../types/csprng";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { UserKey, MasterKey, PinKey } from "../../types/key";
|
import { UserKey, MasterKey, PinKey } from "../../types/key";
|
||||||
@ -17,7 +18,7 @@ import { EncString } from "../models/domain/enc-string";
|
|||||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||||
import { CryptoService } from "../services/crypto.service";
|
import { CryptoService } from "../services/crypto.service";
|
||||||
|
|
||||||
import { USER_EVER_HAD_USER_KEY } from "./key-state/user-key.state";
|
import { USER_EVER_HAD_USER_KEY, USER_KEY } from "./key-state/user-key.state";
|
||||||
|
|
||||||
describe("cryptoService", () => {
|
describe("cryptoService", () => {
|
||||||
let cryptoService: CryptoService;
|
let cryptoService: CryptoService;
|
||||||
@ -57,42 +58,50 @@ describe("cryptoService", () => {
|
|||||||
|
|
||||||
describe("getUserKey", () => {
|
describe("getUserKey", () => {
|
||||||
let mockUserKey: UserKey;
|
let mockUserKey: UserKey;
|
||||||
let stateSvcGetUserKey: jest.SpyInstance;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||||
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||||
|
});
|
||||||
|
|
||||||
stateSvcGetUserKey = jest.spyOn(stateService, "getUserKey");
|
it("retrieves the key state of the requested user", async () => {
|
||||||
|
await cryptoService.getUserKey(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(USER_KEY, mockUserId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the User Key if available", async () => {
|
it("returns the User Key if available", async () => {
|
||||||
stateSvcGetUserKey.mockResolvedValue(mockUserKey);
|
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
|
||||||
|
|
||||||
const userKey = await cryptoService.getUserKey(mockUserId);
|
const userKey = await cryptoService.getUserKey(mockUserId);
|
||||||
|
|
||||||
expect(stateSvcGetUserKey).toHaveBeenCalledWith({ userId: mockUserId });
|
|
||||||
expect(userKey).toEqual(mockUserKey);
|
expect(userKey).toEqual(mockUserKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets the Auto key if the User Key if not set", async () => {
|
it("sets from the Auto key if the User Key if not set", async () => {
|
||||||
const autoKeyB64 =
|
const autoKeyB64 =
|
||||||
"IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g==";
|
"IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g==";
|
||||||
stateService.getUserKeyAutoUnlock.mockResolvedValue(autoKeyB64);
|
stateService.getUserKeyAutoUnlock.mockResolvedValue(autoKeyB64);
|
||||||
|
const setKeySpy = jest.spyOn(cryptoService, "setUserKey");
|
||||||
|
|
||||||
const userKey = await cryptoService.getUserKey(mockUserId);
|
const userKey = await cryptoService.getUserKey(mockUserId);
|
||||||
|
|
||||||
expect(stateService.setUserKey).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), {
|
expect(setKeySpy).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), mockUserId);
|
||||||
userId: mockUserId,
|
expect(setKeySpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
|
||||||
expect(userKey.keyB64).toEqual(autoKeyB64);
|
expect(userKey.keyB64).toEqual(autoKeyB64);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns nullish if there is no auto key and the user key is not set", async () => {
|
||||||
|
const userKey = await cryptoService.getUserKey(mockUserId);
|
||||||
|
|
||||||
|
expect(userKey).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getUserKeyWithLegacySupport", () => {
|
describe("getUserKeyWithLegacySupport", () => {
|
||||||
let mockUserKey: UserKey;
|
let mockUserKey: UserKey;
|
||||||
let mockMasterKey: MasterKey;
|
let mockMasterKey: MasterKey;
|
||||||
let stateSvcGetUserKey: jest.SpyInstance;
|
|
||||||
let stateSvcGetMasterKey: jest.SpyInstance;
|
let stateSvcGetMasterKey: jest.SpyInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -100,23 +109,22 @@ describe("cryptoService", () => {
|
|||||||
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||||
mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
||||||
|
|
||||||
stateSvcGetUserKey = jest.spyOn(stateService, "getUserKey");
|
|
||||||
stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey");
|
stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the User Key if available", async () => {
|
it("returns the User Key if available", async () => {
|
||||||
stateSvcGetUserKey.mockResolvedValue(mockUserKey);
|
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
|
||||||
|
const getKeySpy = jest.spyOn(cryptoService, "getUserKey");
|
||||||
|
|
||||||
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
|
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
|
||||||
|
|
||||||
expect(stateSvcGetUserKey).toHaveBeenCalledWith({ userId: mockUserId });
|
expect(getKeySpy).toHaveBeenCalledWith(mockUserId);
|
||||||
expect(stateSvcGetMasterKey).not.toHaveBeenCalled();
|
expect(stateSvcGetMasterKey).not.toHaveBeenCalled();
|
||||||
|
|
||||||
expect(userKey).toEqual(mockUserKey);
|
expect(userKey).toEqual(mockUserKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the user's master key when User Key is not available", async () => {
|
it("returns the user's master key when User Key is not available", async () => {
|
||||||
stateSvcGetUserKey.mockResolvedValue(null);
|
|
||||||
stateSvcGetMasterKey.mockResolvedValue(mockMasterKey);
|
stateSvcGetMasterKey.mockResolvedValue(mockMasterKey);
|
||||||
|
|
||||||
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
|
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
|
||||||
@ -201,6 +209,19 @@ describe("cryptoService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("throws if key is null", async () => {
|
||||||
|
await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update the user's lock state", async () => {
|
||||||
|
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
||||||
|
|
||||||
|
expect(accountService.mock.setAccountStatus).toHaveBeenCalledWith(
|
||||||
|
mockUserId,
|
||||||
|
AuthenticationStatus.Unlocked,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe("Pin Key refresh", () => {
|
describe("Pin Key refresh", () => {
|
||||||
let cryptoSvcMakePinKey: jest.SpyInstance;
|
let cryptoSvcMakePinKey: jest.SpyInstance;
|
||||||
const protectedPin =
|
const protectedPin =
|
||||||
@ -259,4 +280,36 @@ describe("cryptoService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("clearUserKey", () => {
|
||||||
|
it.each([mockUserId, null])("should clear the User Key for id %2", async (userId) => {
|
||||||
|
await cryptoService.clearUserKey(false, userId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update status to locked", async () => {
|
||||||
|
await cryptoService.clearUserKey(false, mockUserId);
|
||||||
|
|
||||||
|
expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith(
|
||||||
|
mockUserId,
|
||||||
|
AuthenticationStatus.Locked,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([true, false])(
|
||||||
|
"should clear stored user keys if clearAll is true (%s)",
|
||||||
|
async (clear) => {
|
||||||
|
const clearSpy = (cryptoService["clearAllStoredUserKeys"] = jest.fn());
|
||||||
|
await cryptoService.clearUserKey(clear, mockUserId);
|
||||||
|
|
||||||
|
if (clear) {
|
||||||
|
expect(clearSpy).toHaveBeenCalledWith(mockUserId);
|
||||||
|
expect(clearSpy).toHaveBeenCalledTimes(1);
|
||||||
|
} else {
|
||||||
|
expect(clearSpy).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,7 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response
|
|||||||
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
||||||
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
|
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
|
||||||
@ -52,9 +53,11 @@ import {
|
|||||||
USER_EVER_HAD_USER_KEY,
|
USER_EVER_HAD_USER_KEY,
|
||||||
USER_PRIVATE_KEY,
|
USER_PRIVATE_KEY,
|
||||||
USER_PUBLIC_KEY,
|
USER_PUBLIC_KEY,
|
||||||
|
USER_KEY,
|
||||||
} from "./key-state/user-key.state";
|
} from "./key-state/user-key.state";
|
||||||
|
|
||||||
export class CryptoService implements CryptoServiceAbstraction {
|
export class CryptoService implements CryptoServiceAbstraction {
|
||||||
|
private readonly activeUserKeyState: ActiveUserState<UserKey>;
|
||||||
private readonly activeUserEverHadUserKey: ActiveUserState<boolean>;
|
private readonly activeUserEverHadUserKey: ActiveUserState<boolean>;
|
||||||
private readonly activeUserEncryptedOrgKeysState: ActiveUserState<
|
private readonly activeUserEncryptedOrgKeysState: ActiveUserState<
|
||||||
Record<OrganizationId, EncryptedOrganizationKeyData>
|
Record<OrganizationId, EncryptedOrganizationKeyData>
|
||||||
@ -68,6 +71,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
private readonly activeUserPrivateKeyState: DerivedState<UserPrivateKey>;
|
private readonly activeUserPrivateKeyState: DerivedState<UserPrivateKey>;
|
||||||
private readonly activeUserPublicKeyState: DerivedState<UserPublicKey>;
|
private readonly activeUserPublicKeyState: DerivedState<UserPublicKey>;
|
||||||
|
|
||||||
|
readonly activeUserKey$: Observable<UserKey>;
|
||||||
|
|
||||||
readonly activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
|
readonly activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
|
||||||
readonly activeUserProviderKeys$: Observable<Record<ProviderId, ProviderKey>>;
|
readonly activeUserProviderKeys$: Observable<Record<ProviderId, ProviderKey>>;
|
||||||
readonly activeUserPrivateKey$: Observable<UserPrivateKey>;
|
readonly activeUserPrivateKey$: Observable<UserPrivateKey>;
|
||||||
@ -84,6 +89,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
protected stateProvider: StateProvider,
|
protected stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
// User Key
|
// User Key
|
||||||
|
this.activeUserKeyState = stateProvider.getActive(USER_KEY);
|
||||||
|
this.activeUserKey$ = this.activeUserKeyState.state$;
|
||||||
this.activeUserEverHadUserKey = stateProvider.getActive(USER_EVER_HAD_USER_KEY);
|
this.activeUserEverHadUserKey = stateProvider.getActive(USER_EVER_HAD_USER_KEY);
|
||||||
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
|
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
|
||||||
|
|
||||||
@ -131,13 +138,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUserKey(key: UserKey, userId?: UserId): Promise<void> {
|
async setUserKey(key: UserKey, userId?: UserId): Promise<void> {
|
||||||
// TODO: make this non-nullable in signature
|
if (key == null) {
|
||||||
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
throw new Error("No key provided. Use ClearUserKey to clear the key");
|
||||||
if (key != null) {
|
|
||||||
// Key should never be null anyway
|
|
||||||
await this.stateProvider.getUser(userId, USER_EVER_HAD_USER_KEY).update(() => true);
|
|
||||||
}
|
}
|
||||||
await this.stateService.setUserKey(key, { userId: userId });
|
// Set userId to ensure we have one for the account status update
|
||||||
|
[userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId);
|
||||||
|
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId);
|
||||||
|
|
||||||
|
await this.accountService.setAccountStatus(userId, AuthenticationStatus.Unlocked);
|
||||||
|
|
||||||
await this.storeAdditionalKeys(key, userId);
|
await this.storeAdditionalKeys(key, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +156,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserKey(userId?: UserId): Promise<UserKey> {
|
async getUserKey(userId?: UserId): Promise<UserKey> {
|
||||||
let userKey = await this.stateService.getUserKey({ userId: userId });
|
let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
return userKey;
|
return userKey;
|
||||||
}
|
}
|
||||||
@ -197,7 +206,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async hasUserKeyInMemory(userId?: UserId): Promise<boolean> {
|
async hasUserKeyInMemory(userId?: UserId): Promise<boolean> {
|
||||||
return (await this.stateService.getUserKey({ userId: userId })) != null;
|
return (await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId))) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||||
@ -215,7 +224,9 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clearUserKey(clearStoredKeys = true, userId?: UserId): Promise<void> {
|
async clearUserKey(clearStoredKeys = true, userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setUserKey(null, { userId: userId });
|
// Set userId to ensure we have one for the account status update
|
||||||
|
[userId] = await this.stateProvider.setUserState(USER_KEY, null, userId);
|
||||||
|
await this.accountService.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
|
||||||
if (clearStoredKeys) {
|
if (clearStoredKeys) {
|
||||||
await this.clearAllStoredUserKeys(userId);
|
await this.clearAllStoredUserKeys(userId);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { UserPrivateKey, UserPublicKey } from "../../../types/key";
|
import { UserPrivateKey, UserPublicKey, UserKey } from "../../../types/key";
|
||||||
import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
|
||||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||||
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
||||||
import { KeyDefinition, CRYPTO_DISK, DeriveDefinition } from "../../state";
|
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||||
|
import { KeyDefinition, CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY } from "../../state";
|
||||||
import { CryptoService } from "../crypto.service";
|
import { CryptoService } from "../crypto.service";
|
||||||
|
|
||||||
export const USER_EVER_HAD_USER_KEY = new KeyDefinition<boolean>(CRYPTO_DISK, "everHadUserKey", {
|
export const USER_EVER_HAD_USER_KEY = new KeyDefinition<boolean>(CRYPTO_DISK, "everHadUserKey", {
|
||||||
@ -57,3 +58,6 @@ export const USER_PUBLIC_KEY = DeriveDefinition.from<
|
|||||||
return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
|
return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
export const USER_KEY = new KeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", {
|
||||||
|
deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey,
|
||||||
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BehaviorSubject, concatMap } from "rxjs";
|
import { BehaviorSubject, Observable, map } from "rxjs";
|
||||||
import { Jsonify, JsonValue } from "type-fest";
|
import { Jsonify, JsonValue } from "type-fest";
|
||||||
|
|
||||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||||
@ -20,7 +20,7 @@ import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
|||||||
import { SendData } from "../../tools/send/models/data/send.data";
|
import { SendData } from "../../tools/send/models/data/send.data";
|
||||||
import { SendView } from "../../tools/send/models/view/send.view";
|
import { SendView } from "../../tools/send/models/view/send.view";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { DeviceKey, MasterKey, UserKey } from "../../types/key";
|
import { DeviceKey, MasterKey } from "../../types/key";
|
||||||
import { UriMatchType } from "../../vault/enums";
|
import { UriMatchType } from "../../vault/enums";
|
||||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||||
import { LocalData } from "../../vault/models/data/local.data";
|
import { LocalData } from "../../vault/models/data/local.data";
|
||||||
@ -87,8 +87,7 @@ export class StateService<
|
|||||||
protected activeAccountSubject = new BehaviorSubject<string | null>(null);
|
protected activeAccountSubject = new BehaviorSubject<string | null>(null);
|
||||||
activeAccount$ = this.activeAccountSubject.asObservable();
|
activeAccount$ = this.activeAccountSubject.asObservable();
|
||||||
|
|
||||||
protected activeAccountUnlockedSubject = new BehaviorSubject<boolean>(false);
|
activeAccountUnlocked$: Observable<boolean>;
|
||||||
activeAccountUnlocked$ = this.activeAccountUnlockedSubject.asObservable();
|
|
||||||
|
|
||||||
private hasBeenInited = false;
|
private hasBeenInited = false;
|
||||||
protected isRecoveredSession = false;
|
protected isRecoveredSession = false;
|
||||||
@ -109,22 +108,11 @@ export class StateService<
|
|||||||
private migrationRunner: MigrationRunner,
|
private migrationRunner: MigrationRunner,
|
||||||
protected useAccountCache: boolean = true,
|
protected useAccountCache: boolean = true,
|
||||||
) {
|
) {
|
||||||
// If the account gets changed, verify the new account is unlocked
|
this.activeAccountUnlocked$ = this.accountService.activeAccount$.pipe(
|
||||||
this.activeAccountSubject
|
map((a) => {
|
||||||
.pipe(
|
return a?.status === AuthenticationStatus.Unlocked;
|
||||||
concatMap(async (userId) => {
|
}),
|
||||||
if (userId == null && this.activeAccountUnlockedSubject.getValue() == false) {
|
);
|
||||||
return;
|
|
||||||
} else if (userId == null) {
|
|
||||||
this.activeAccountUnlockedSubject.next(false);
|
|
||||||
}
|
|
||||||
// FIXME: This should be refactored into AuthService or a similar service,
|
|
||||||
// as checking for the existence of the crypto key is a low level
|
|
||||||
// implementation detail.
|
|
||||||
this.activeAccountUnlockedSubject.next((await this.getUserKey()) != null);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(initOptions: InitOptions = {}): Promise<void> {
|
async init(initOptions: InitOptions = {}): Promise<void> {
|
||||||
@ -522,68 +510,6 @@ export class StateService<
|
|||||||
return account?.keys?.cryptoMasterKey;
|
return account?.keys?.cryptoMasterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Do not save the Master Key. Use the User Symmetric Key instead
|
|
||||||
*/
|
|
||||||
async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
account.keys.cryptoMasterKey = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextStatus = value != null ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked;
|
|
||||||
await this.accountService.setAccountStatus(options.userId as UserId, nextStatus);
|
|
||||||
|
|
||||||
if (options.userId == this.activeAccountSubject.getValue()) {
|
|
||||||
const nextValue = value != null;
|
|
||||||
|
|
||||||
// Avoid emitting if we are already unlocked
|
|
||||||
if (this.activeAccountUnlockedSubject.getValue() != nextValue) {
|
|
||||||
this.activeAccountUnlockedSubject.next(nextValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* user key used to encrypt/decrypt data
|
|
||||||
*/
|
|
||||||
async getUserKey(options?: StorageOptions): Promise<UserKey> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
return account?.keys?.userKey as UserKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* user key used to encrypt/decrypt data
|
|
||||||
*/
|
|
||||||
async setUserKey(value: UserKey, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
account.keys.userKey = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextStatus = value != null ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked;
|
|
||||||
await this.accountService.setAccountStatus(options.userId as UserId, nextStatus);
|
|
||||||
|
|
||||||
if (options?.userId == this.activeAccountSubject.getValue()) {
|
|
||||||
const nextValue = value != null;
|
|
||||||
|
|
||||||
// Avoid emitting if we are already unlocked
|
|
||||||
if (this.activeAccountUnlockedSubject.getValue() != nextValue) {
|
|
||||||
this.activeAccountUnlockedSubject.next(nextValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User's master key derived from MP, saved only if we decrypted with MP
|
* User's master key derived from MP, saved only if we decrypted with MP
|
||||||
*/
|
*/
|
||||||
@ -885,33 +811,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use UserKey instead
|
|
||||||
*/
|
|
||||||
async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
return account?.keys?.cryptoSymmetricKey?.decrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use UserKey instead
|
|
||||||
*/
|
|
||||||
async setDecryptedCryptoSymmetricKey(
|
|
||||||
value: SymmetricCryptoKey,
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
account.keys.cryptoSymmetricKey.decrypted = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@withPrototypeForArrayMembers(GeneratedPasswordHistory)
|
@withPrototypeForArrayMembers(GeneratedPasswordHistory)
|
||||||
async getDecryptedPasswordGenerationHistory(
|
async getDecryptedPasswordGenerationHistory(
|
||||||
options?: StorageOptions,
|
options?: StorageOptions,
|
||||||
@ -1565,20 +1464,6 @@ export class StateService<
|
|||||||
)?.keys.cryptoSymmetricKey.encrypted;
|
)?.keys.cryptoSymmetricKey.encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use UserKey instead
|
|
||||||
*/
|
|
||||||
async setEncryptedCryptoSymmetricKey(value: string, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.keys.cryptoSymmetricKey.encrypted = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@withPrototypeForArrayMembers(GeneratedPasswordHistory)
|
@withPrototypeForArrayMembers(GeneratedPasswordHistory)
|
||||||
async getEncryptedPasswordGenerationHistory(
|
async getEncryptedPasswordGenerationHistory(
|
||||||
options?: StorageOptions,
|
options?: StorageOptions,
|
||||||
|
@ -22,6 +22,7 @@ export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
|||||||
export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk");
|
export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk");
|
||||||
|
|
||||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||||
|
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
||||||
|
|
||||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||||
|
|
||||||
|
@ -40,6 +40,18 @@ export class MigrationHelper {
|
|||||||
return this.storageService.save(key, value);
|
return this.storageService.save(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a value in the storage service at the given key.
|
||||||
|
*
|
||||||
|
* This is a brute force method to just remove a value in the storage service. If you can use {@link removeFromGlobal} or {@link removeFromUser}, you should.
|
||||||
|
* @param key location
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
remove(key: string): Promise<void> {
|
||||||
|
this.logService.info(`Removing ${key}`);
|
||||||
|
return this.storageService.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a globally scoped value from a location derived through the key definition
|
* Gets a globally scoped value from a location derived through the key definition
|
||||||
*
|
*
|
||||||
@ -65,6 +77,18 @@ export class MigrationHelper {
|
|||||||
return this.set(this.getGlobalKey(keyDefinition), value);
|
return this.set(this.getGlobalKey(keyDefinition), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a globally scoped location derived through the key definition
|
||||||
|
*
|
||||||
|
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
|
||||||
|
* use {@link remove} for those.
|
||||||
|
* @param keyDefinition unique key definition
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
removeFromGlobal(keyDefinition: KeyDefinitionLike): Promise<void> {
|
||||||
|
return this.remove(this.getGlobalKey(keyDefinition));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a user scoped value from a location derived through the user id and key definition
|
* Gets a user scoped value from a location derived through the user id and key definition
|
||||||
*
|
*
|
||||||
@ -92,6 +116,18 @@ export class MigrationHelper {
|
|||||||
return this.set(this.getUserKey(userId, keyDefinition), value);
|
return this.set(this.getUserKey(userId, keyDefinition), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a user scoped location derived through the key definition
|
||||||
|
*
|
||||||
|
* This is for use with the state providers framework, DO NOT use for values stored with {@link StateService},
|
||||||
|
* use {@link remove} for those.
|
||||||
|
* @param keyDefinition unique key definition
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
removeFromUser(userId: string, keyDefinition: KeyDefinitionLike): Promise<void> {
|
||||||
|
return this.remove(this.getUserKey(userId, keyDefinition));
|
||||||
|
}
|
||||||
|
|
||||||
info(message: string): void {
|
info(message: string): void {
|
||||||
this.logService.info(message);
|
this.logService.info(message);
|
||||||
}
|
}
|
||||||
|
@ -230,10 +230,8 @@ describe("PSONO JSON Importer", () => {
|
|||||||
const result = await importer.parse(FoldersTestDataJson);
|
const result = await importer.parse(FoldersTestDataJson);
|
||||||
expect(result != null).toBe(true);
|
expect(result != null).toBe(true);
|
||||||
|
|
||||||
const folders = result.folders;
|
|
||||||
// // Check that ciphers have a folder assigned to them
|
// // Check that ciphers have a folder assigned to them
|
||||||
expect(result.ciphers.filter((c) => c.folderId === folders[0].id).length).toBeGreaterThan(0);
|
expect(result.ciphers.length).toEqual(result.folderRelationships.length);
|
||||||
expect(result.ciphers.filter((c) => c.folderId === folders[1].id).length).toBeGreaterThan(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create collections if part of an organization", async () => {
|
it("should create collections if part of an organization", async () => {
|
||||||
|
@ -23,6 +23,24 @@ export const FoldersTestData: PsonoJsonExport = {
|
|||||||
callback_user: "callbackUser",
|
callback_user: "callbackUser",
|
||||||
callback_pass: "callbackPassword",
|
callback_pass: "callbackPassword",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "website_password",
|
||||||
|
name: "TestEntry1.2",
|
||||||
|
autosubmit: true,
|
||||||
|
urlfilter: "filter",
|
||||||
|
website_password_title: "TestEntry1.2",
|
||||||
|
website_password_url: "bitwarden.com",
|
||||||
|
website_password_username: "testUser",
|
||||||
|
website_password_password: "testPassword",
|
||||||
|
website_password_notes: "some notes",
|
||||||
|
website_password_auto_submit: true,
|
||||||
|
website_password_url_filter: "filter",
|
||||||
|
create_date: "2022-12-13T19:24:09.810266Z",
|
||||||
|
write_date: "2022-12-13T19:24:09.810292Z",
|
||||||
|
callback_url: "callback",
|
||||||
|
callback_user: "callbackUser",
|
||||||
|
callback_pass: "callbackPassword",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -61,20 +61,22 @@ export class PsonoJsonImporter extends BaseImporter implements Importer {
|
|||||||
this.parseFolders(result, folder.folders, folder.name);
|
this.parseFolders(result, folder.folders, folder.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processFolder(result, folder.name, folderHasItems);
|
if (!folderHasItems) {
|
||||||
|
this.processFolder(result, folder.name, folderHasItems);
|
||||||
this.handleItemParsing(result, folder.items);
|
} else {
|
||||||
|
this.handleItemParsing(result, folder.items, folder.name);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleItemParsing(result: ImportResult, items?: PsonoItemTypes[]) {
|
private handleItemParsing(result: ImportResult, items?: PsonoItemTypes[], folderName?: string) {
|
||||||
if (items == null || items.length === 0) {
|
if (items == null || items.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.forEach((record) => {
|
items.forEach((record) => {
|
||||||
const cipher = this.parsePsonoItem(record);
|
const cipher = this.parsePsonoItem(record);
|
||||||
|
this.processFolder(result, folderName, true);
|
||||||
this.cleanupCipher(cipher);
|
this.cleanupCipher(cipher);
|
||||||
result.ciphers.push(cipher);
|
result.ciphers.push(cipher);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user