mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
Specify clearOn options for platform services (#8584)
* Use UserKeys in biometric state * Remove global clear todo. Answer is never * User UserKeys in crypto state * Clear userkey on both lock and logout via User Key Definitions * Use UserKeyDefinitions in environment service * Rely on userKeyDefinition to clear org keys * Rely on userKeyDefinition to clear provider keys * Rely on userKeyDefinition to clear user keys * Rely on userKeyDefinitions to clear user asym key pair
This commit is contained in:
parent
aefea43fff
commit
c02723d6a6
@ -32,12 +32,8 @@ export class FakeAccountService implements AccountService {
|
||||
get activeUserId() {
|
||||
return this._activeUserId;
|
||||
}
|
||||
get accounts$() {
|
||||
return this.accountsSubject.asObservable();
|
||||
}
|
||||
get activeAccount$() {
|
||||
return this.activeAccountSubject.asObservable();
|
||||
}
|
||||
accounts$ = this.accountsSubject.asObservable();
|
||||
activeAccount$ = this.activeAccountSubject.asObservable();
|
||||
accountLock$: Observable<UserId>;
|
||||
accountLogout$: Observable<UserId>;
|
||||
|
||||
|
@ -26,7 +26,7 @@ export abstract class CryptoService {
|
||||
* any other necessary versions (such as auto, biometrics,
|
||||
* or pin)
|
||||
*
|
||||
* @throws when key is null. Use {@link clearUserKey} instead
|
||||
* @throws when key is null. Lock the account to clear a key
|
||||
* @param key The user key to set
|
||||
* @param userId The desired user
|
||||
*/
|
||||
@ -93,13 +93,6 @@ export abstract class CryptoService {
|
||||
* @returns A new user key and the master key protected version of it
|
||||
*/
|
||||
abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>;
|
||||
/**
|
||||
* Clears the user key
|
||||
* @param clearStoredKeys Clears all stored versions of the user keys as well,
|
||||
* such as the biometrics key
|
||||
* @param userId The desired user
|
||||
*/
|
||||
abstract clearUserKey(clearSecretStorage?: boolean, userId?: string): Promise<void>;
|
||||
/**
|
||||
* Clears the user's stored version of the user key
|
||||
* @param keySuffix The desired version of the key to clear
|
||||
@ -238,12 +231,6 @@ export abstract class CryptoService {
|
||||
abstract makeDataEncKey<T extends UserKey | OrgKey>(
|
||||
key: T,
|
||||
): Promise<[SymmetricCryptoKey, EncString]>;
|
||||
/**
|
||||
* Clears the user's stored organization keys
|
||||
* @param memoryOnly Clear only the in-memory keys
|
||||
* @param userId The desired user
|
||||
*/
|
||||
abstract clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise<void>;
|
||||
/**
|
||||
* Stores the encrypted provider keys and clears any decrypted
|
||||
* provider keys currently in memory
|
||||
@ -260,11 +247,6 @@ export abstract class CryptoService {
|
||||
* @returns A record of the provider Ids to their symmetric keys
|
||||
*/
|
||||
abstract getProviderKeys(): Promise<Record<ProviderId, ProviderKey>>;
|
||||
/**
|
||||
* @param memoryOnly Clear only the in-memory keys
|
||||
* @param userId The desired user
|
||||
*/
|
||||
abstract clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise<void>;
|
||||
/**
|
||||
* Returns the public key from memory. If not available, extracts it
|
||||
* from the private key and stores it in memory
|
||||
@ -304,12 +286,6 @@ export abstract class CryptoService {
|
||||
* @returns A new keypair: [publicKey in Base64, encrypted privateKey]
|
||||
*/
|
||||
abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>;
|
||||
/**
|
||||
* Clears the user's key pair
|
||||
* @param memoryOnly Clear only the in-memory keys
|
||||
* @param userId The desired user
|
||||
*/
|
||||
abstract clearKeyPair(memoryOnly?: boolean, userId?: string): Promise<void[]>;
|
||||
/**
|
||||
* @param pin The user's pin
|
||||
* @param salt The user's salt
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { EncryptedString } from "../models/domain/enc-string";
|
||||
import { KeyDefinition } from "../state";
|
||||
import { KeyDefinition, UserKeyDefinition } from "../state";
|
||||
|
||||
import {
|
||||
BIOMETRIC_UNLOCK_ENABLED,
|
||||
@ -22,9 +22,15 @@ describe.each([
|
||||
])(
|
||||
"deserializes state %s",
|
||||
(
|
||||
...args: [KeyDefinition<EncryptedString>, EncryptedString] | [KeyDefinition<boolean>, boolean]
|
||||
...args:
|
||||
| [UserKeyDefinition<EncryptedString>, EncryptedString]
|
||||
| [UserKeyDefinition<boolean>, boolean]
|
||||
| [KeyDefinition<boolean>, boolean]
|
||||
) => {
|
||||
function testDeserialization<T>(keyDefinition: KeyDefinition<T>, state: T) {
|
||||
function testDeserialization<T>(
|
||||
keyDefinition: UserKeyDefinition<T> | KeyDefinition<T>,
|
||||
state: T,
|
||||
) {
|
||||
const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state)));
|
||||
expect(deserialized).toEqual(state);
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { UserId } from "../../types/guid";
|
||||
import { EncryptedString } from "../models/domain/enc-string";
|
||||
import { KeyDefinition, BIOMETRIC_SETTINGS_DISK } from "../state";
|
||||
import { KeyDefinition, BIOMETRIC_SETTINGS_DISK, UserKeyDefinition } from "../state";
|
||||
|
||||
/**
|
||||
* Indicates whether the user elected to store a biometric key to unlock their vault.
|
||||
*/
|
||||
export const BIOMETRIC_UNLOCK_ENABLED = new KeyDefinition<boolean>(
|
||||
export const BIOMETRIC_UNLOCK_ENABLED = new UserKeyDefinition<boolean>(
|
||||
BIOMETRIC_SETTINGS_DISK,
|
||||
"biometricUnlockEnabled",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
|
||||
@ -18,11 +19,12 @@ export const BIOMETRIC_UNLOCK_ENABLED = new KeyDefinition<boolean>(
|
||||
*
|
||||
* A true setting controls whether {@link ENCRYPTED_CLIENT_KEY_HALF} is set.
|
||||
*/
|
||||
export const REQUIRE_PASSWORD_ON_START = new KeyDefinition<boolean>(
|
||||
export const REQUIRE_PASSWORD_ON_START = new UserKeyDefinition<boolean>(
|
||||
BIOMETRIC_SETTINGS_DISK,
|
||||
"requirePasswordOnStart",
|
||||
{
|
||||
deserializer: (value) => value,
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
|
||||
@ -33,11 +35,12 @@ export const REQUIRE_PASSWORD_ON_START = new KeyDefinition<boolean>(
|
||||
* For operating systems without application-level key storage, this key half is concatenated with a signature
|
||||
* provided by the OS and used to encrypt the biometric key prior to storage.
|
||||
*/
|
||||
export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition<EncryptedString>(
|
||||
export const ENCRYPTED_CLIENT_KEY_HALF = new UserKeyDefinition<EncryptedString>(
|
||||
BIOMETRIC_SETTINGS_DISK,
|
||||
"clientKeyHalf",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
@ -45,11 +48,12 @@ export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition<EncryptedString>(
|
||||
* Indicates the user has been warned about the security implications of using biometrics and, depending on the OS,
|
||||
* recommended to require a password on first unlock of an application instance.
|
||||
*/
|
||||
export const DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT = new KeyDefinition<boolean>(
|
||||
export const DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT = new UserKeyDefinition<boolean>(
|
||||
BIOMETRIC_SETTINGS_DISK,
|
||||
"dismissedBiometricRequirePasswordOnStartCallout",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
|
||||
@ -68,11 +72,12 @@ export const PROMPT_CANCELLED = KeyDefinition.record<boolean, UserId>(
|
||||
/**
|
||||
* Stores whether the user has elected to automatically prompt for biometric unlock on application start.
|
||||
*/
|
||||
export const PROMPT_AUTOMATICALLY = new KeyDefinition<boolean>(
|
||||
export const PROMPT_AUTOMATICALLY = new UserKeyDefinition<boolean>(
|
||||
BIOMETRIC_SETTINGS_DISK,
|
||||
"promptAutomatically",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -32,7 +32,6 @@ export const USER_SERVER_CONFIG = new UserKeyDefinition<ServerConfig>(CONFIG_DIS
|
||||
clearOn: ["logout"],
|
||||
});
|
||||
|
||||
// TODO MDG: When to clean these up?
|
||||
export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record<ServerConfig, ApiUrl>(
|
||||
CONFIG_DISK,
|
||||
"byServer",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { firstValueFrom, of, tap } from "rxjs";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||
@ -18,6 +18,7 @@ import { Utils } from "../misc/utils";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CryptoService } from "../services/crypto.service";
|
||||
import { UserKeyDefinition } from "../state";
|
||||
|
||||
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state";
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state";
|
||||
@ -336,231 +337,22 @@ 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,
|
||||
describe("clearKeys", () => {
|
||||
it("resolves active user id when called with no user id", async () => {
|
||||
let callCount = 0;
|
||||
accountService.activeAccount$ = accountService.activeAccountSubject.pipe(
|
||||
tap(() => callCount++),
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
await cryptoService.clearKeys(null);
|
||||
expect(callCount).toBe(1);
|
||||
|
||||
if (clear) {
|
||||
expect(clearSpy).toHaveBeenCalledWith(mockUserId);
|
||||
expect(clearSpy).toHaveBeenCalledTimes(1);
|
||||
} else {
|
||||
expect(clearSpy).not.toHaveBeenCalled();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("clearOrgKeys", () => {
|
||||
let forceMemorySpy: jest.Mock;
|
||||
beforeEach(() => {
|
||||
forceMemorySpy = cryptoService["activeUserOrgKeysState"].forceValue = jest.fn();
|
||||
});
|
||||
it("clears in memory org keys when called with memoryOnly", async () => {
|
||||
await cryptoService.clearOrgKeys(true);
|
||||
|
||||
expect(forceMemorySpy).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it("does not clear memory when called with the non active user and memory only", async () => {
|
||||
await cryptoService.clearOrgKeys(true, "someOtherUser" as UserId);
|
||||
|
||||
expect(forceMemorySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not write to disk state if called with memory only", async () => {
|
||||
await cryptoService.clearOrgKeys(true);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears disk state when called with diskOnly", async () => {
|
||||
await cryptoService.clearOrgKeys(false);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
);
|
||||
expect(
|
||||
stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_ORGANIZATION_KEYS).nextMock,
|
||||
).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("clears another user's disk state when called with diskOnly and that user", async () => {
|
||||
await cryptoService.clearOrgKeys(false, "someOtherUser" as UserId);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith(
|
||||
"someOtherUser" as UserId,
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
);
|
||||
expect(
|
||||
stateProvider.singleUser.getFake(
|
||||
"someOtherUser" as UserId,
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
).nextMock,
|
||||
).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("does not clear active user disk state when called with diskOnly and a different specified user", async () => {
|
||||
await cryptoService.clearOrgKeys(false, "someOtherUser" as UserId);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearProviderKeys", () => {
|
||||
let forceMemorySpy: jest.Mock;
|
||||
beforeEach(() => {
|
||||
forceMemorySpy = cryptoService["activeUserProviderKeysState"].forceValue = jest.fn();
|
||||
});
|
||||
it("clears in memory org keys when called with memoryOnly", async () => {
|
||||
await cryptoService.clearProviderKeys(true);
|
||||
|
||||
expect(forceMemorySpy).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it("does not clear memory when called with the non active user and memory only", async () => {
|
||||
await cryptoService.clearProviderKeys(true, "someOtherUser" as UserId);
|
||||
|
||||
expect(forceMemorySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not write to disk state if called with memory only", async () => {
|
||||
await cryptoService.clearProviderKeys(true);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears disk state when called with diskOnly", async () => {
|
||||
await cryptoService.clearProviderKeys(false);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||
);
|
||||
expect(
|
||||
stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PROVIDER_KEYS).nextMock,
|
||||
).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("clears another user's disk state when called with diskOnly and that user", async () => {
|
||||
await cryptoService.clearProviderKeys(false, "someOtherUser" as UserId);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith(
|
||||
"someOtherUser" as UserId,
|
||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||
);
|
||||
expect(
|
||||
stateProvider.singleUser.getFake("someOtherUser" as UserId, USER_ENCRYPTED_PROVIDER_KEYS)
|
||||
.nextMock,
|
||||
).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("does not clear active user disk state when called with diskOnly and a different specified user", async () => {
|
||||
await cryptoService.clearProviderKeys(false, "someOtherUser" as UserId);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearKeyPair", () => {
|
||||
let forceMemoryPrivateKeySpy: jest.Mock;
|
||||
let forceMemoryPublicKeySpy: jest.Mock;
|
||||
beforeEach(() => {
|
||||
forceMemoryPrivateKeySpy = cryptoService["activeUserPrivateKeyState"].forceValue = jest.fn();
|
||||
forceMemoryPublicKeySpy = cryptoService["activeUserPublicKeyState"].forceValue = jest.fn();
|
||||
});
|
||||
it("clears in memory org keys when called with memoryOnly", async () => {
|
||||
await cryptoService.clearKeyPair(true);
|
||||
|
||||
expect(forceMemoryPrivateKeySpy).toHaveBeenCalledWith(null);
|
||||
expect(forceMemoryPublicKeySpy).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("does not clear memory when called with the non active user and memory only", async () => {
|
||||
await cryptoService.clearKeyPair(true, "someOtherUser" as UserId);
|
||||
|
||||
expect(forceMemoryPrivateKeySpy).not.toHaveBeenCalled();
|
||||
expect(forceMemoryPublicKeySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not write to disk state if called with memory only", async () => {
|
||||
await cryptoService.clearKeyPair(true);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears disk state when called with diskOnly", async () => {
|
||||
await cryptoService.clearKeyPair(false);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
expect(
|
||||
stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY).nextMock,
|
||||
).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("clears another user's disk state when called with diskOnly and that user", async () => {
|
||||
await cryptoService.clearKeyPair(false, "someOtherUser" as UserId);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith(
|
||||
"someOtherUser" as UserId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
expect(
|
||||
stateProvider.singleUser.getFake("someOtherUser" as UserId, USER_ENCRYPTED_PRIVATE_KEY)
|
||||
.nextMock,
|
||||
).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("does not clear active user disk state when called with diskOnly and a different specified user", async () => {
|
||||
await cryptoService.clearKeyPair(false, "someOtherUser" as UserId);
|
||||
|
||||
expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearUserKey", () => {
|
||||
it("clears the user key for the active user when no userId is specified", async () => {
|
||||
await cryptoService.clearUserKey(false);
|
||||
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, undefined);
|
||||
});
|
||||
|
||||
it("clears the user key for the specified user when a userId is specified", async () => {
|
||||
await cryptoService.clearUserKey(false, "someOtherUser" as UserId);
|
||||
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, "someOtherUser");
|
||||
// revert to the original state
|
||||
accountService.activeAccount$ = accountService.activeAccountSubject.asObservable();
|
||||
});
|
||||
|
||||
it("sets the maximum account status of the active user id to locked when user id is not specified", async () => {
|
||||
await cryptoService.clearUserKey(false);
|
||||
await cryptoService.clearKeys();
|
||||
expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
AuthenticationStatus.Locked,
|
||||
@ -568,17 +360,36 @@ describe("cryptoService", () => {
|
||||
});
|
||||
|
||||
it("sets the maximum account status of the specified user id to locked when user id is specified", async () => {
|
||||
await cryptoService.clearUserKey(false, "someOtherUser" as UserId);
|
||||
const userId = "someOtherUser" as UserId;
|
||||
await cryptoService.clearKeys(userId);
|
||||
expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith(
|
||||
"someOtherUser" as UserId,
|
||||
userId,
|
||||
AuthenticationStatus.Locked,
|
||||
);
|
||||
});
|
||||
|
||||
it("clears all stored user keys when clearAll is true", async () => {
|
||||
const clearAllSpy = (cryptoService["clearAllStoredUserKeys"] = jest.fn());
|
||||
await cryptoService.clearUserKey(true);
|
||||
expect(clearAllSpy).toHaveBeenCalledWith(mockUserId);
|
||||
describe.each([
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
USER_KEY,
|
||||
])("key removal", (key: UserKeyDefinition<unknown>) => {
|
||||
it(`clears ${key.key} for active user when unspecified`, async () => {
|
||||
await cryptoService.clearKeys(null);
|
||||
|
||||
const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key);
|
||||
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it(`clears ${key.key} for the specified user when specified`, async () => {
|
||||
const userId = "someOtherUser" as UserId;
|
||||
await cryptoService.clearKeys(userId);
|
||||
|
||||
const encryptedOrgKeyState = stateProvider.singleUser.getFake(userId, key);
|
||||
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -144,7 +144,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
|
||||
async setUserKey(key: UserKey, userId?: UserId): Promise<void> {
|
||||
if (key == null) {
|
||||
throw new Error("No key provided. Use ClearUserKey to clear the key");
|
||||
throw new Error("No key provided. Lock the user to clear the key");
|
||||
}
|
||||
// Set userId to ensure we have one for the account status update
|
||||
[userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId);
|
||||
@ -242,13 +242,19 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return this.buildProtectedSymmetricKey(masterKey, newUserKey.key);
|
||||
}
|
||||
|
||||
async clearUserKey(clearStoredKeys = true, userId?: UserId): Promise<void> {
|
||||
// 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) {
|
||||
await this.clearAllStoredUserKeys(userId);
|
||||
/**
|
||||
* Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key
|
||||
* @param userId The desired user
|
||||
*/
|
||||
async clearUserKey(userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
// Set userId to ensure we have one for the account status update
|
||||
await this.stateProvider.setUserState(USER_KEY, null, userId);
|
||||
await this.accountService.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
|
||||
await this.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
|
||||
async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
|
||||
@ -480,25 +486,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return this.buildProtectedSymmetricKey(key, newSymKey.key);
|
||||
}
|
||||
|
||||
async clearOrgKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> {
|
||||
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const userIdIsActive = userId == null || userId === activeUserId;
|
||||
|
||||
if (!memoryOnly) {
|
||||
if (userId == null && activeUserId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
await this.stateProvider
|
||||
.getUser(userId ?? activeUserId, USER_ENCRYPTED_ORGANIZATION_KEYS)
|
||||
.update(() => null);
|
||||
private async clearOrgKeys(userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// org keys are only cached for active users
|
||||
if (userIdIsActive) {
|
||||
await this.activeUserOrgKeysState.forceValue({});
|
||||
}
|
||||
await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId);
|
||||
}
|
||||
|
||||
async setProviderKeys(providers: ProfileProviderResponse[]): Promise<void> {
|
||||
@ -526,25 +519,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return await firstValueFrom(this.activeUserProviderKeys$);
|
||||
}
|
||||
|
||||
async clearProviderKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> {
|
||||
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const userIdIsActive = userId == null || userId === activeUserId;
|
||||
|
||||
if (!memoryOnly) {
|
||||
if (userId == null && activeUserId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
await this.stateProvider
|
||||
.getUser(userId ?? activeUserId, USER_ENCRYPTED_PROVIDER_KEYS)
|
||||
.update(() => null);
|
||||
private async clearProviderKeys(userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// provider keys are only cached for active users
|
||||
if (userIdIsActive) {
|
||||
await this.activeUserProviderKeysState.forceValue({});
|
||||
}
|
||||
await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId);
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<Uint8Array> {
|
||||
@ -597,26 +577,17 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return [publicB64, privateEnc];
|
||||
}
|
||||
|
||||
async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise<void[]> {
|
||||
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const userIdIsActive = userId == null || userId === activeUserId;
|
||||
|
||||
if (!memoryOnly) {
|
||||
if (userId == null && activeUserId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
await this.stateProvider
|
||||
.getUser(userId ?? activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
|
||||
.update(() => null);
|
||||
/**
|
||||
* Clears the user's key pair
|
||||
* @param userId The desired user
|
||||
*/
|
||||
private async clearKeyPair(userId: UserId): Promise<void[]> {
|
||||
if (userId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// decrypted key pair is only cached for active users
|
||||
if (userIdIsActive) {
|
||||
await this.activeUserPrivateKeyState.forceValue(null);
|
||||
await this.activeUserPublicKeyState.forceValue(null);
|
||||
}
|
||||
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
|
||||
}
|
||||
|
||||
async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise<PinKey> {
|
||||
@ -681,11 +652,17 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async clearKeys(userId?: UserId): Promise<any> {
|
||||
await this.clearUserKey(true, userId);
|
||||
userId ||= (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
if (userId == null) {
|
||||
throw new Error("Cannot clear keys, no user Id resolved.");
|
||||
}
|
||||
|
||||
await this.clearUserKey(userId);
|
||||
await this.clearMasterKeyHash(userId);
|
||||
await this.clearOrgKeys(false, userId);
|
||||
await this.clearProviderKeys(false, userId);
|
||||
await this.clearKeyPair(false, userId);
|
||||
await this.clearOrgKeys(userId);
|
||||
await this.clearProviderKeys(userId);
|
||||
await this.clearKeyPair(userId);
|
||||
await this.clearPinKeys(userId);
|
||||
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
|
||||
}
|
||||
|
@ -7,9 +7,10 @@ import { UserId } from "../../types/guid";
|
||||
import { CloudRegion, Region } from "../abstractions/environment.service";
|
||||
|
||||
import {
|
||||
ENVIRONMENT_KEY,
|
||||
GLOBAL_ENVIRONMENT_KEY,
|
||||
DefaultEnvironmentService,
|
||||
EnvironmentUrls,
|
||||
USER_ENVIRONMENT_KEY,
|
||||
} from "./default-environment.service";
|
||||
|
||||
// There are a few main states EnvironmentService could be in when first used
|
||||
@ -55,7 +56,7 @@ describe("EnvironmentService", () => {
|
||||
};
|
||||
|
||||
const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => {
|
||||
stateProvider.global.getFake(ENVIRONMENT_KEY).stateSubject.next({
|
||||
stateProvider.global.getFake(GLOBAL_ENVIRONMENT_KEY).stateSubject.next({
|
||||
region: region,
|
||||
urls: environmentUrls,
|
||||
});
|
||||
@ -66,7 +67,7 @@ describe("EnvironmentService", () => {
|
||||
environmentUrls: EnvironmentUrls,
|
||||
userId: UserId = testUser,
|
||||
) => {
|
||||
stateProvider.singleUser.getFake(userId, ENVIRONMENT_KEY).nextState({
|
||||
stateProvider.singleUser.getFake(userId, USER_ENVIRONMENT_KEY).nextState({
|
||||
region: region,
|
||||
urls: environmentUrls,
|
||||
});
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
GlobalState,
|
||||
KeyDefinition,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../state";
|
||||
|
||||
export class EnvironmentUrls {
|
||||
@ -40,7 +41,7 @@ class EnvironmentState {
|
||||
}
|
||||
}
|
||||
|
||||
export const ENVIRONMENT_KEY = new KeyDefinition<EnvironmentState>(
|
||||
export const GLOBAL_ENVIRONMENT_KEY = new KeyDefinition<EnvironmentState>(
|
||||
ENVIRONMENT_DISK,
|
||||
"environment",
|
||||
{
|
||||
@ -48,9 +49,31 @@ export const ENVIRONMENT_KEY = new KeyDefinition<EnvironmentState>(
|
||||
},
|
||||
);
|
||||
|
||||
export const CLOUD_REGION_KEY = new KeyDefinition<CloudRegion>(ENVIRONMENT_MEMORY, "cloudRegion", {
|
||||
deserializer: (b) => b,
|
||||
});
|
||||
export const USER_ENVIRONMENT_KEY = new UserKeyDefinition<EnvironmentState>(
|
||||
ENVIRONMENT_DISK,
|
||||
"environment",
|
||||
{
|
||||
deserializer: EnvironmentState.fromJSON,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export const GLOBAL_CLOUD_REGION_KEY = new KeyDefinition<CloudRegion>(
|
||||
ENVIRONMENT_MEMORY,
|
||||
"cloudRegion",
|
||||
{
|
||||
deserializer: (b) => b,
|
||||
},
|
||||
);
|
||||
|
||||
export const USER_CLOUD_REGION_KEY = new UserKeyDefinition<CloudRegion>(
|
||||
ENVIRONMENT_MEMORY,
|
||||
"cloudRegion",
|
||||
{
|
||||
deserializer: (b) => b,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The production regions available for selection.
|
||||
@ -114,8 +137,8 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
private stateProvider: StateProvider,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.globalState = this.stateProvider.getGlobal(ENVIRONMENT_KEY);
|
||||
this.globalCloudRegionState = this.stateProvider.getGlobal(CLOUD_REGION_KEY);
|
||||
this.globalState = this.stateProvider.getGlobal(GLOBAL_ENVIRONMENT_KEY);
|
||||
this.globalCloudRegionState = this.stateProvider.getGlobal(GLOBAL_CLOUD_REGION_KEY);
|
||||
|
||||
const account$ = this.activeAccountId$.pipe(
|
||||
// Use == here to not trigger on undefined -> null transition
|
||||
@ -125,8 +148,8 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
this.environment$ = account$.pipe(
|
||||
switchMap((userId) => {
|
||||
const t = userId
|
||||
? this.stateProvider.getUser(userId, ENVIRONMENT_KEY).state$
|
||||
: this.stateProvider.getGlobal(ENVIRONMENT_KEY).state$;
|
||||
? this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$
|
||||
: this.stateProvider.getGlobal(GLOBAL_ENVIRONMENT_KEY).state$;
|
||||
return t;
|
||||
}),
|
||||
map((state) => {
|
||||
@ -136,8 +159,8 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
this.cloudWebVaultUrl$ = account$.pipe(
|
||||
switchMap((userId) => {
|
||||
const t = userId
|
||||
? this.stateProvider.getUser(userId, CLOUD_REGION_KEY).state$
|
||||
: this.stateProvider.getGlobal(CLOUD_REGION_KEY).state$;
|
||||
? this.stateProvider.getUser(userId, USER_CLOUD_REGION_KEY).state$
|
||||
: this.stateProvider.getGlobal(GLOBAL_CLOUD_REGION_KEY).state$;
|
||||
return t;
|
||||
}),
|
||||
map((region) => {
|
||||
@ -242,7 +265,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
if (userId == null) {
|
||||
await this.globalCloudRegionState.update(() => region);
|
||||
} else {
|
||||
await this.stateProvider.getUser(userId, CLOUD_REGION_KEY).update(() => region);
|
||||
await this.stateProvider.getUser(userId, USER_CLOUD_REGION_KEY).update(() => region);
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,13 +284,13 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
return activeUserId == null
|
||||
? await firstValueFrom(this.globalState.state$)
|
||||
: await firstValueFrom(
|
||||
this.stateProvider.getUser(userId ?? activeUserId, ENVIRONMENT_KEY).state$,
|
||||
this.stateProvider.getUser(userId ?? activeUserId, USER_ENVIRONMENT_KEY).state$,
|
||||
);
|
||||
}
|
||||
|
||||
async seedUserEnvironment(userId: UserId) {
|
||||
const global = await firstValueFrom(this.globalState.state$);
|
||||
await this.stateProvider.getUser(userId, ENVIRONMENT_KEY).update(() => global);
|
||||
await this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).update(() => global);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,13 +4,14 @@ import { OrganizationId } from "../../../types/guid";
|
||||
import { OrgKey } from "../../../types/key";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { KeyDefinition, CRYPTO_DISK, DeriveDefinition } from "../../state";
|
||||
import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state";
|
||||
|
||||
export const USER_ENCRYPTED_ORGANIZATION_KEYS = KeyDefinition.record<
|
||||
export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record<
|
||||
EncryptedOrganizationKeyData,
|
||||
OrganizationId
|
||||
>(CRYPTO_DISK, "organizationKeys", {
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
});
|
||||
|
||||
export const USER_ORGANIZATION_KEYS = DeriveDefinition.from<
|
||||
|
@ -3,14 +3,15 @@ import { ProviderKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { KeyDefinition, CRYPTO_DISK, DeriveDefinition } from "../../state";
|
||||
import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state";
|
||||
import { CryptoService } from "../crypto.service";
|
||||
|
||||
export const USER_ENCRYPTED_PROVIDER_KEYS = KeyDefinition.record<EncryptedString, ProviderId>(
|
||||
export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record<EncryptedString, ProviderId>(
|
||||
CRYPTO_DISK,
|
||||
"providerKeys",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -3,18 +3,25 @@ import { CryptoFunctionService } from "../../abstractions/crypto-function.servic
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { KeyDefinition, CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY } from "../../state";
|
||||
import {
|
||||
KeyDefinition,
|
||||
CRYPTO_DISK,
|
||||
DeriveDefinition,
|
||||
CRYPTO_MEMORY,
|
||||
UserKeyDefinition,
|
||||
} from "../../state";
|
||||
import { CryptoService } from "../crypto.service";
|
||||
|
||||
export const USER_EVER_HAD_USER_KEY = new KeyDefinition<boolean>(CRYPTO_DISK, "everHadUserKey", {
|
||||
deserializer: (obj) => obj,
|
||||
});
|
||||
|
||||
export const USER_ENCRYPTED_PRIVATE_KEY = new KeyDefinition<EncryptedString>(
|
||||
export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition<EncryptedString>(
|
||||
CRYPTO_DISK,
|
||||
"privateKey",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
@ -58,6 +65,7 @@ export const USER_PUBLIC_KEY = DeriveDefinition.from<
|
||||
return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
|
||||
},
|
||||
});
|
||||
export const USER_KEY = new KeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", {
|
||||
export const USER_KEY = new UserKeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", {
|
||||
deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey,
|
||||
clearOn: ["logout", "lock"],
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import { DerivedStateDependencies, StorageKey } from "../../types/state";
|
||||
|
||||
import { KeyDefinition } from "./key-definition";
|
||||
import { StateDefinition } from "./state-definition";
|
||||
import { UserKeyDefinition } from "./user-key-definition";
|
||||
|
||||
declare const depShapeMarker: unique symbol;
|
||||
/**
|
||||
@ -129,26 +130,28 @@ export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies
|
||||
static from<TFrom, TTo, TDeps extends DerivedStateDependencies = never>(
|
||||
definition:
|
||||
| KeyDefinition<TFrom>
|
||||
| UserKeyDefinition<TFrom>
|
||||
| [DeriveDefinition<unknown, TFrom, DerivedStateDependencies>, string],
|
||||
options: DeriveDefinitionOptions<TFrom, TTo, TDeps>,
|
||||
) {
|
||||
if (isKeyDefinition(definition)) {
|
||||
return new DeriveDefinition(definition.stateDefinition, definition.key, options);
|
||||
} else {
|
||||
if (isFromDeriveDefinition(definition)) {
|
||||
return new DeriveDefinition(definition[0].stateDefinition, definition[1], options);
|
||||
} else {
|
||||
return new DeriveDefinition(definition.stateDefinition, definition.key, options);
|
||||
}
|
||||
}
|
||||
|
||||
static fromWithUserId<TKeyDef, TTo, TDeps extends DerivedStateDependencies = never>(
|
||||
definition:
|
||||
| KeyDefinition<TKeyDef>
|
||||
| UserKeyDefinition<TKeyDef>
|
||||
| [DeriveDefinition<unknown, TKeyDef, DerivedStateDependencies>, string],
|
||||
options: DeriveDefinitionOptions<[UserId, TKeyDef], TTo, TDeps>,
|
||||
) {
|
||||
if (isKeyDefinition(definition)) {
|
||||
return new DeriveDefinition(definition.stateDefinition, definition.key, options);
|
||||
} else {
|
||||
if (isFromDeriveDefinition(definition)) {
|
||||
return new DeriveDefinition(definition[0].stateDefinition, definition[1], options);
|
||||
} else {
|
||||
return new DeriveDefinition(definition.stateDefinition, definition.key, options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,10 +184,11 @@ export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies
|
||||
}
|
||||
}
|
||||
|
||||
function isKeyDefinition(
|
||||
function isFromDeriveDefinition(
|
||||
definition:
|
||||
| KeyDefinition<unknown>
|
||||
| UserKeyDefinition<unknown>
|
||||
| [DeriveDefinition<unknown, unknown, DerivedStateDependencies>, string],
|
||||
): definition is KeyDefinition<unknown> {
|
||||
return Object.prototype.hasOwnProperty.call(definition, "key");
|
||||
): definition is [DeriveDefinition<unknown, unknown, DerivedStateDependencies>, string] {
|
||||
return Array.isArray(definition);
|
||||
}
|
||||
|
@ -156,7 +156,6 @@ describe("VaultTimeoutService", () => {
|
||||
expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId);
|
||||
expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId });
|
||||
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId });
|
||||
expect(cryptoService.clearUserKey).toHaveBeenCalledWith(false, userId);
|
||||
expect(cryptoService.clearMasterKey).toHaveBeenCalledWith(userId);
|
||||
expect(cipherService.clearCache).toHaveBeenCalledWith(userId);
|
||||
expect(lockedCallback).toHaveBeenCalledWith(userId);
|
||||
|
@ -96,10 +96,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
|
||||
await this.cryptoService.clearUserKey(false, userId);
|
||||
await this.cryptoService.clearMasterKey(userId);
|
||||
await this.cryptoService.clearOrgKeys(true, userId);
|
||||
await this.cryptoService.clearKeyPair(true, userId);
|
||||
|
||||
await this.cipherService.clearCache(userId);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user