mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
[PM-5533] Migrate Asymmetric User Keys to State Providers (#7665)
This commit is contained in:
@@ -225,8 +225,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
* @deprecated Do not call this, use PolicyService
|
||||
*/
|
||||
setDecryptedPolicies: (value: Policy[], options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedPrivateKey: (options?: StorageOptions) => Promise<Uint8Array>;
|
||||
setDecryptedPrivateKey: (value: Uint8Array, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use SendService
|
||||
*/
|
||||
@@ -346,8 +344,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
value: { [id: string]: PolicyData },
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getEncryptedPrivateKey: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedPrivateKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use SendService
|
||||
*/
|
||||
@@ -434,8 +430,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
|
||||
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
|
||||
getPublicKey: (options?: StorageOptions) => Promise<Uint8Array>;
|
||||
setPublicKey: (value: Uint8Array, options?: StorageOptions) => Promise<void>;
|
||||
getRefreshToken: (options?: StorageOptions) => Promise<string>;
|
||||
setRefreshToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getRememberedEmail: (options?: StorageOptions) => Promise<string>;
|
||||
|
||||
@@ -125,7 +125,6 @@ export class AccountKeys {
|
||||
masterKey?: MasterKey;
|
||||
masterKeyEncryptedUserKey?: string;
|
||||
deviceKey?: ReturnType<SymmetricCryptoKey["toJSON"]>;
|
||||
privateKey?: EncryptionPair<string, Uint8Array> = new EncryptionPair<string, Uint8Array>();
|
||||
publicKey?: Uint8Array;
|
||||
apiKeyClientSecret?: string;
|
||||
|
||||
@@ -163,9 +162,6 @@ export class AccountKeys {
|
||||
obj?.cryptoSymmetricKey,
|
||||
SymmetricCryptoKey.fromJSON,
|
||||
),
|
||||
privateKey: EncryptionPair.fromJSON<string, Uint8Array>(obj?.privateKey, (decObj: string) =>
|
||||
Utils.fromByteStringToArray(decObj),
|
||||
),
|
||||
publicKey: Utils.fromByteStringToArray(obj?.publicKey),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,16 @@ import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
|
||||
import { OrgKey, UserKey, MasterKey, ProviderKey, PinKey, CipherKey } from "../../types/key";
|
||||
import {
|
||||
OrgKey,
|
||||
UserKey,
|
||||
MasterKey,
|
||||
ProviderKey,
|
||||
PinKey,
|
||||
CipherKey,
|
||||
UserPrivateKey,
|
||||
UserPublicKey,
|
||||
} from "../../types/key";
|
||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
@@ -38,7 +47,12 @@ import {
|
||||
USER_ORGANIZATION_KEYS,
|
||||
} from "./key-state/org-keys.state";
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS, USER_PROVIDER_KEYS } from "./key-state/provider-keys.state";
|
||||
import { USER_EVER_HAD_USER_KEY } from "./key-state/user-key.state";
|
||||
import {
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
USER_EVER_HAD_USER_KEY,
|
||||
USER_PRIVATE_KEY,
|
||||
USER_PUBLIC_KEY,
|
||||
} from "./key-state/user-key.state";
|
||||
|
||||
export class CryptoService implements CryptoServiceAbstraction {
|
||||
private readonly activeUserEverHadUserKey: ActiveUserState<boolean>;
|
||||
@@ -49,12 +63,16 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
private readonly activeUserEncryptedProviderKeysState: ActiveUserState<
|
||||
Record<ProviderId, EncryptedString>
|
||||
>;
|
||||
private readonly activeUserProviderKeysState: DerivedState<Record<OrganizationId, ProviderKey>>;
|
||||
private readonly activeUserProviderKeysState: DerivedState<Record<ProviderId, ProviderKey>>;
|
||||
private readonly activeUserEncryptedPrivateKeyState: ActiveUserState<EncryptedString>;
|
||||
private readonly activeUserPrivateKeyState: DerivedState<UserPrivateKey>;
|
||||
private readonly activeUserPublicKeyState: DerivedState<UserPublicKey>;
|
||||
|
||||
readonly activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
|
||||
readonly activeUserProviderKeys$: Observable<Record<ProviderId, ProviderKey>>;
|
||||
|
||||
readonly everHadUserKey$;
|
||||
readonly activeUserPrivateKey$: Observable<UserPrivateKey>;
|
||||
readonly activeUserPublicKey$: Observable<UserPublicKey>;
|
||||
readonly everHadUserKey$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
@@ -65,7 +83,31 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
protected accountService: AccountService,
|
||||
protected stateProvider: StateProvider,
|
||||
) {
|
||||
// User Key
|
||||
this.activeUserEverHadUserKey = stateProvider.getActive(USER_EVER_HAD_USER_KEY);
|
||||
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
|
||||
|
||||
// User Asymmetric Key Pair
|
||||
this.activeUserEncryptedPrivateKeyState = stateProvider.getActive(USER_ENCRYPTED_PRIVATE_KEY);
|
||||
this.activeUserPrivateKeyState = stateProvider.getDerived(
|
||||
this.activeUserEncryptedPrivateKeyState.combinedState$,
|
||||
USER_PRIVATE_KEY,
|
||||
{
|
||||
encryptService: this.encryptService,
|
||||
cryptoService: this,
|
||||
},
|
||||
);
|
||||
this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null
|
||||
this.activeUserPublicKeyState = stateProvider.getDerived(
|
||||
this.activeUserPrivateKey$,
|
||||
USER_PUBLIC_KEY,
|
||||
{
|
||||
cryptoFunctionService: this.cryptoFunctionService,
|
||||
},
|
||||
);
|
||||
this.activeUserPublicKey$ = this.activeUserPublicKeyState.state$; // may be null
|
||||
|
||||
// Organization keys
|
||||
this.activeUserEncryptedOrgKeysState = stateProvider.getActive(
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
);
|
||||
@@ -74,6 +116,9 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
USER_ORGANIZATION_KEYS,
|
||||
{ cryptoService: this },
|
||||
);
|
||||
this.activeUserOrgKeys$ = this.activeUserOrgKeysState.state$; // null handled by `derive` function
|
||||
|
||||
// Provider keys
|
||||
this.activeUserEncryptedProviderKeysState = stateProvider.getActive(
|
||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||
);
|
||||
@@ -82,9 +127,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
USER_PROVIDER_KEYS,
|
||||
{ encryptService: this.encryptService, cryptoService: this },
|
||||
);
|
||||
|
||||
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
|
||||
this.activeUserOrgKeys$ = this.activeUserOrgKeysState.state$; // null handled by `derive` function
|
||||
this.activeUserProviderKeys$ = this.activeUserProviderKeysState.state$; // null handled by `derive` function
|
||||
}
|
||||
|
||||
@@ -465,19 +507,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async getPublicKey(): Promise<Uint8Array> {
|
||||
const inMemoryPublicKey = await this.stateService.getPublicKey();
|
||||
if (inMemoryPublicKey != null) {
|
||||
return inMemoryPublicKey;
|
||||
}
|
||||
|
||||
const privateKey = await this.getPrivateKey();
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
await this.stateService.setPublicKey(publicKey);
|
||||
return publicKey;
|
||||
return await firstValueFrom(this.activeUserPublicKey$);
|
||||
}
|
||||
|
||||
async makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]> {
|
||||
@@ -487,32 +517,16 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return [encShareKey, new SymmetricCryptoKey(shareKey) as T];
|
||||
}
|
||||
|
||||
async setPrivateKey(encPrivateKey: string): Promise<void> {
|
||||
async setPrivateKey(encPrivateKey: EncryptedString): Promise<void> {
|
||||
if (encPrivateKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stateService.setDecryptedPrivateKey(null);
|
||||
await this.stateService.setEncryptedPrivateKey(encPrivateKey);
|
||||
await this.activeUserEncryptedPrivateKeyState.update(() => encPrivateKey);
|
||||
}
|
||||
|
||||
async getPrivateKey(): Promise<Uint8Array> {
|
||||
const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey();
|
||||
if (decryptedPrivateKey != null) {
|
||||
return decryptedPrivateKey;
|
||||
}
|
||||
|
||||
const encPrivateKey = await this.stateService.getEncryptedPrivateKey();
|
||||
if (encPrivateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const privateKey = await this.encryptService.decryptToBytes(
|
||||
new EncString(encPrivateKey),
|
||||
await this.getUserKeyWithLegacySupport(),
|
||||
);
|
||||
await this.stateService.setDecryptedPrivateKey(privateKey);
|
||||
return privateKey;
|
||||
return await firstValueFrom(this.activeUserPrivateKey$);
|
||||
}
|
||||
|
||||
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
|
||||
@@ -543,14 +557,23 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise<void[]> {
|
||||
const keysToClear: Promise<void>[] = [
|
||||
this.stateService.setDecryptedPrivateKey(null, { userId: userId }),
|
||||
this.stateService.setPublicKey(null, { userId: userId }),
|
||||
];
|
||||
if (!memoryOnly) {
|
||||
keysToClear.push(this.stateService.setEncryptedPrivateKey(null, { userId: userId }));
|
||||
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const userIdIsActive = userId == null || userId === activeUserId;
|
||||
if (memoryOnly && userIdIsActive) {
|
||||
// key pair is only cached for active users
|
||||
await this.activeUserPrivateKeyState.forceValue(null);
|
||||
await this.activeUserPublicKeyState.forceValue(null);
|
||||
return;
|
||||
} else {
|
||||
if (userId == null && activeUserId == null) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
// below updates decrypted private key and public keys if this is the active user as well since those are derived from the encrypted private key
|
||||
await this.stateProvider
|
||||
.getUser(userId ?? activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
|
||||
.update(() => null);
|
||||
}
|
||||
return Promise.all(keysToClear);
|
||||
}
|
||||
|
||||
async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise<PinKey> {
|
||||
@@ -735,16 +758,23 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
try {
|
||||
const encPrivateKey = await this.stateService.getEncryptedPrivateKey();
|
||||
const [userId, encPrivateKey] = await firstValueFrom(
|
||||
this.activeUserEncryptedPrivateKeyState.combinedState$,
|
||||
);
|
||||
if (encPrivateKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const privateKey = await this.encryptService.decryptToBytes(
|
||||
new EncString(encPrivateKey),
|
||||
key,
|
||||
);
|
||||
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
// Can decrypt private key
|
||||
const privateKey = await USER_PRIVATE_KEY.derive([userId, encPrivateKey], {
|
||||
encryptService: this.encryptService,
|
||||
cryptoService: this,
|
||||
});
|
||||
|
||||
// Can successfully derive public key
|
||||
await USER_PUBLIC_KEY.derive(privateKey, {
|
||||
cryptoFunctionService: this.cryptoFunctionService,
|
||||
});
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
@@ -765,7 +795,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
const userKey = new SymmetricCryptoKey(rawKey) as UserKey;
|
||||
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
||||
await this.setUserKey(userKey);
|
||||
await this.stateService.setEncryptedPrivateKey(privateKey.encryptedString);
|
||||
await this.activeUserEncryptedPrivateKeyState.update(() => privateKey.encryptedString);
|
||||
|
||||
return {
|
||||
userKey,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey, UserPrivateKey, UserPublicKey } from "../../../types/key";
|
||||
import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { EncString } from "../../models/domain/enc-string";
|
||||
import { CryptoService } from "../crypto.service";
|
||||
|
||||
import {
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
USER_EVER_HAD_USER_KEY,
|
||||
USER_PRIVATE_KEY,
|
||||
USER_PUBLIC_KEY,
|
||||
} from "./user-key.state";
|
||||
|
||||
function makeEncString(data?: string) {
|
||||
data ??= Utils.newGuid();
|
||||
return new EncString(EncryptionType.AesCbc256_HmacSha256_B64, data, "test", "test");
|
||||
}
|
||||
|
||||
describe("Ever had user key", () => {
|
||||
const sut = USER_EVER_HAD_USER_KEY;
|
||||
|
||||
it("should deserialize ever had user key", () => {
|
||||
const everHadUserKey = true;
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(everHadUserKey)));
|
||||
|
||||
expect(result).toEqual(everHadUserKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Encrypted private key", () => {
|
||||
const sut = USER_ENCRYPTED_PRIVATE_KEY;
|
||||
|
||||
it("should deserialize encrypted private key", () => {
|
||||
const encryptedPrivateKey = makeEncString().encryptedString;
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(encryptedPrivateKey)));
|
||||
|
||||
expect(result).toEqual(encryptedPrivateKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("User public key", () => {
|
||||
const sut = USER_PUBLIC_KEY;
|
||||
const userPrivateKey = makeStaticByteArray(64, 1) as UserPrivateKey;
|
||||
const userPublicKey = makeStaticByteArray(64, 2) as UserPublicKey;
|
||||
|
||||
it("should deserialize user public key", () => {
|
||||
const userPublicKey = makeStaticByteArray(64, 1);
|
||||
|
||||
const result = sut.deserialize(JSON.parse(JSON.stringify(userPublicKey)));
|
||||
|
||||
expect(result).toEqual(userPublicKey);
|
||||
});
|
||||
|
||||
it("should derive user public key", async () => {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(userPublicKey);
|
||||
|
||||
const result = await sut.derive(userPrivateKey, { cryptoFunctionService });
|
||||
|
||||
expect(result).toEqual(userPublicKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Derived decrypted private key", () => {
|
||||
const sut = USER_PRIVATE_KEY;
|
||||
const userId = "userId" as UserId;
|
||||
const userKey = mock<UserKey>();
|
||||
const encryptedPrivateKey = makeEncString().encryptedString;
|
||||
const decryptedPrivateKey = makeStaticByteArray(64, 1);
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should deserialize decrypted private key", () => {
|
||||
const decryptedPrivateKey = makeStaticByteArray(64, 1);
|
||||
|
||||
const result = sut.deserialize(JSON.parse(JSON.stringify(decryptedPrivateKey)));
|
||||
|
||||
expect(result).toEqual(decryptedPrivateKey);
|
||||
});
|
||||
|
||||
it("should derive decrypted private key", async () => {
|
||||
const cryptoService = mock<CryptoService>();
|
||||
cryptoService.getUserKey.mockResolvedValue(userKey);
|
||||
const encryptService = mock<EncryptService>();
|
||||
encryptService.decryptToBytes.mockResolvedValue(decryptedPrivateKey);
|
||||
|
||||
const result = await sut.derive([userId, encryptedPrivateKey], {
|
||||
encryptService,
|
||||
cryptoService,
|
||||
});
|
||||
|
||||
expect(result).toEqual(decryptedPrivateKey);
|
||||
});
|
||||
|
||||
it("should handle null input values", async () => {
|
||||
const cryptoService = mock<CryptoService>();
|
||||
cryptoService.getUserKey.mockResolvedValue(userKey);
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
const result = await sut.derive([userId, null], {
|
||||
encryptService,
|
||||
cryptoService,
|
||||
});
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
it("should handle null user key", async () => {
|
||||
const cryptoService = mock<CryptoService>();
|
||||
cryptoService.getUserKey.mockResolvedValue(null);
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
const result = await sut.derive([userId, encryptedPrivateKey], {
|
||||
encryptService,
|
||||
cryptoService,
|
||||
});
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,59 @@
|
||||
import { KeyDefinition, CRYPTO_DISK } from "../../state";
|
||||
import { UserPrivateKey, UserPublicKey } from "../../../types/key";
|
||||
import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { EncString, EncryptedString } from "../../models/domain/enc-string";
|
||||
import { KeyDefinition, CRYPTO_DISK, DeriveDefinition } 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>(
|
||||
CRYPTO_DISK,
|
||||
"privateKey",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
},
|
||||
);
|
||||
|
||||
export const USER_PRIVATE_KEY = DeriveDefinition.fromWithUserId<
|
||||
EncryptedString,
|
||||
UserPrivateKey,
|
||||
// TODO: update cryptoService to user key directly
|
||||
{ encryptService: EncryptService; cryptoService: CryptoService }
|
||||
>(USER_ENCRYPTED_PRIVATE_KEY, {
|
||||
deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPrivateKey,
|
||||
derive: async ([userId, encPrivateKeyString], { encryptService, cryptoService }) => {
|
||||
if (encPrivateKeyString == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userKey = await cryptoService.getUserKey(userId);
|
||||
if (userKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encPrivateKey = new EncString(encPrivateKeyString);
|
||||
const privateKey = (await encryptService.decryptToBytes(
|
||||
encPrivateKey,
|
||||
userKey,
|
||||
)) as UserPrivateKey;
|
||||
return privateKey;
|
||||
},
|
||||
});
|
||||
|
||||
export const USER_PUBLIC_KEY = DeriveDefinition.from<
|
||||
UserPrivateKey,
|
||||
UserPublicKey,
|
||||
{ cryptoFunctionService: CryptoFunctionService }
|
||||
>([USER_PRIVATE_KEY, "publicKey"], {
|
||||
deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPublicKey,
|
||||
derive: async (privateKey, { cryptoFunctionService }) => {
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1048,23 +1048,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getDecryptedPrivateKey(options?: StorageOptions): Promise<Uint8Array> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
)?.keys?.privateKey.decrypted;
|
||||
}
|
||||
|
||||
async setDecryptedPrivateKey(value: Uint8Array, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
account.keys.privateKey.decrypted = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
@withPrototypeForArrayMembers(SendView)
|
||||
async getDecryptedSends(options?: StorageOptions): Promise<SendView[]> {
|
||||
return (
|
||||
@@ -1752,24 +1735,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getEncryptedPrivateKey(options?: StorageOptions): Promise<string> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
return account?.keys?.privateKey?.encrypted;
|
||||
}
|
||||
|
||||
async setEncryptedPrivateKey(value: string, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
account.keys.privateKey.encrypted = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
@withPrototypeForObjectValues(SendData)
|
||||
async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> {
|
||||
return (
|
||||
@@ -2274,24 +2239,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getPublicKey(options?: StorageOptions): Promise<Uint8Array> {
|
||||
const keys = (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
)?.keys;
|
||||
return keys?.publicKey;
|
||||
}
|
||||
|
||||
async setPublicKey(value: Uint8Array, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
account.keys.publicKey = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getRefreshToken(options?: StorageOptions): Promise<string> {
|
||||
options = await this.getTimeoutBasedStorageOptions(options);
|
||||
return (await this.getAccount(options))?.tokens?.refreshToken;
|
||||
|
||||
42
libs/common/src/platform/state/derive-definition.spec.ts
Normal file
42
libs/common/src/platform/state/derive-definition.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { DeriveDefinition } from "./derive-definition";
|
||||
import { KeyDefinition } from "./key-definition";
|
||||
import { StateDefinition } from "./state-definition";
|
||||
|
||||
const derive: () => any = () => null;
|
||||
const deserializer: any = (obj: any) => obj;
|
||||
|
||||
const STATE_DEFINITION = new StateDefinition("test", "disk");
|
||||
const TEST_KEY = new KeyDefinition(STATE_DEFINITION, "test", {
|
||||
deserializer,
|
||||
});
|
||||
const TEST_DERIVE = new DeriveDefinition(STATE_DEFINITION, "test", {
|
||||
derive,
|
||||
deserializer,
|
||||
});
|
||||
|
||||
describe("DeriveDefinition", () => {
|
||||
describe("from", () => {
|
||||
it("should create a new DeriveDefinition from a KeyDefinition", () => {
|
||||
const result = DeriveDefinition.from(TEST_KEY, {
|
||||
derive,
|
||||
deserializer,
|
||||
});
|
||||
|
||||
expect(result).toEqual(TEST_DERIVE);
|
||||
});
|
||||
|
||||
it("should create a new DeriveDefinition from a DeriveDefinition", () => {
|
||||
const result = DeriveDefinition.from([TEST_DERIVE, "newDerive"], {
|
||||
derive,
|
||||
deserializer,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
new DeriveDefinition(STATE_DEFINITION, "newDerive", {
|
||||
derive,
|
||||
deserializer,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
import { DerivedStateDependencies, StorageKey } from "../../types/state";
|
||||
|
||||
import { KeyDefinition } from "./key-definition";
|
||||
@@ -95,18 +96,60 @@ export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Factory that produces a {@link DeriveDefinition} from a {@link KeyDefinition} and a set of options. The returned
|
||||
* definition will have the same key as the given key definition, but will not collide with it in storage, even if
|
||||
* they both reside in memory.
|
||||
* @param keyDefinition
|
||||
* Factory that produces a {@link DeriveDefinition} from a {@link KeyDefinition} or {@link DeriveDefinition} and new name.
|
||||
*
|
||||
* If a `KeyDefinition` is passed in, the returned definition will have the same key as the given key definition, but
|
||||
* will not collide with it in storage, even if they both reside in memory.
|
||||
*
|
||||
* If a `DeriveDefinition` is passed in, the returned definition will instead use the name given in the second position
|
||||
* of the tuple. It is up to you to ensure this is unique within the domain of derived state.
|
||||
*
|
||||
* @param options A set of options to customize the behavior of {@link DeriveDefinition}.
|
||||
* @param options.derive A function to use to convert values from TFrom to TTo. This is called on each emit of the parent state observable
|
||||
* and the resulting value will be emitted from the derived state observable.
|
||||
* @param options.cleanupDelayMs The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed.
|
||||
* Defaults to 1000ms.
|
||||
* @param options.dependencyShape An object defining the dependencies of the derive function. The keys of the object are the names of the dependencies
|
||||
* and the values are the types of the dependencies.
|
||||
* for example:
|
||||
* ```
|
||||
* {
|
||||
* myService: MyService,
|
||||
* myOtherService: MyOtherService,
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param options.deserializer A function to use to safely convert your type from json to your expected type.
|
||||
* Your data may be serialized/deserialized at any time and this needs callback needs to be able to faithfully re-initialize
|
||||
* from the JSON object representation of your type.
|
||||
* @param definition
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
static from<TFrom, TTo, TDeps extends DerivedStateDependencies = never>(
|
||||
keyDefinition: KeyDefinition<TFrom>,
|
||||
definition:
|
||||
| KeyDefinition<TFrom>
|
||||
| [DeriveDefinition<unknown, TFrom, DerivedStateDependencies>, string],
|
||||
options: DeriveDefinitionOptions<TFrom, TTo, TDeps>,
|
||||
) {
|
||||
return new DeriveDefinition(keyDefinition.stateDefinition, keyDefinition.key, options);
|
||||
if (isKeyDefinition(definition)) {
|
||||
return new DeriveDefinition(definition.stateDefinition, definition.key, options);
|
||||
} else {
|
||||
return new DeriveDefinition(definition[0].stateDefinition, definition[1], options);
|
||||
}
|
||||
}
|
||||
|
||||
static fromWithUserId<TKeyDef, TTo, TDeps extends DerivedStateDependencies = never>(
|
||||
definition:
|
||||
| KeyDefinition<TKeyDef>
|
||||
| [DeriveDefinition<unknown, TKeyDef, DerivedStateDependencies>, string],
|
||||
options: DeriveDefinitionOptions<[UserId, TKeyDef], TTo, TDeps>,
|
||||
) {
|
||||
if (isKeyDefinition(definition)) {
|
||||
return new DeriveDefinition(definition.stateDefinition, definition.key, options);
|
||||
} else {
|
||||
return new DeriveDefinition(definition[0].stateDefinition, definition[1], options);
|
||||
}
|
||||
}
|
||||
|
||||
get derive() {
|
||||
@@ -137,3 +180,11 @@ export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies
|
||||
return `derived_${this.stateDefinition.name}_${this.uniqueDerivationName}` as StorageKey;
|
||||
}
|
||||
}
|
||||
|
||||
function isKeyDefinition(
|
||||
definition:
|
||||
| KeyDefinition<unknown>
|
||||
| [DeriveDefinition<unknown, unknown, DerivedStateDependencies>, string],
|
||||
): definition is KeyDefinition<unknown> {
|
||||
return Object.prototype.hasOwnProperty.call(definition, "key");
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export abstract class StateProvider {
|
||||
getGlobal: <T>(keyDefinition: KeyDefinition<T>) => GlobalState<T>;
|
||||
getDerived: <TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||
parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<unknown, TTo, TDeps>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
dependencies: TDeps,
|
||||
) => DerivedState<TTo>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user