mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-16603] Implement userkey rotation v2 (#12646)
* Implement key rotation v2 * Pass through masterpassword hint * Properly split old and new code * Mark legacy rotation as deprecated * Throw when data is null * Cleanup * Add tests * Fix build * Update libs/key-management/src/key.service.spec.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update apps/web/src/app/auth/settings/change-password.component.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add documentation * Centralize loading logic * Fix build * Remove sharedlib from legacymigration component --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
@@ -121,7 +121,7 @@
|
||||
[(ngModel)]="masterPasswordHint"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="loading">
|
||||
{{ "changeMasterPassword" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -12,7 +12,9 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -35,11 +37,13 @@ export class ChangePasswordComponent
|
||||
extends BaseChangePasswordComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
loading = false;
|
||||
rotateUserKey = false;
|
||||
currentMasterPassword: string;
|
||||
masterPasswordHint: string;
|
||||
checkForBreaches = true;
|
||||
characterMinimumMessage = "";
|
||||
userkeyRotationV2 = false;
|
||||
|
||||
constructor(
|
||||
i18nService: I18nService,
|
||||
@@ -56,9 +60,10 @@ export class ChangePasswordComponent
|
||||
private userVerificationService: UserVerificationService,
|
||||
private keyRotationService: UserKeyRotationService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -75,6 +80,8 @@ export class ChangePasswordComponent
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userkeyRotationV2 = await this.configService.getFeatureFlag(FeatureFlag.UserKeyRotationV2);
|
||||
|
||||
if (!(await this.userVerificationService.hasMasterPassword())) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@@ -137,6 +144,130 @@ export class ChangePasswordComponent
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.userkeyRotationV2) {
|
||||
this.loading = true;
|
||||
await this.submitNew();
|
||||
this.loading = false;
|
||||
} else {
|
||||
await this.submitOld();
|
||||
}
|
||||
}
|
||||
|
||||
async submitNew() {
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.masterPasswordHint != null &&
|
||||
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("hintEqualsPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.leakedPassword = false;
|
||||
if (this.checkForBreaches) {
|
||||
this.leakedPassword = (await this.auditService.passwordLeaked(this.masterPassword)) > 0;
|
||||
}
|
||||
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.rotateUserKey) {
|
||||
await this.syncService.fullSync(true);
|
||||
const user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
this.currentMasterPassword,
|
||||
this.masterPassword,
|
||||
user,
|
||||
this.masterPasswordHint,
|
||||
);
|
||||
} else {
|
||||
await this.updatePassword(this.masterPassword);
|
||||
}
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// todo: move this to a service
|
||||
// https://bitwarden.atlassian.net/browse/PM-17108
|
||||
private async updatePassword(newMasterPassword: string) {
|
||||
const currentMasterPassword = this.currentMasterPassword;
|
||||
const { userId, email } = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => ({ userId: a?.id, email: a?.email }))),
|
||||
);
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
|
||||
|
||||
const currentMasterKey = await this.keyService.makeMasterKey(
|
||||
currentMasterPassword,
|
||||
email,
|
||||
kdfConfig,
|
||||
);
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
if (decryptedUserKey == null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
|
||||
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.currentMasterPassword,
|
||||
currentMasterKey,
|
||||
);
|
||||
request.masterPasswordHint = this.masterPasswordHint;
|
||||
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
||||
newMasterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
|
||||
try {
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("masterPasswordChangedDesc"),
|
||||
});
|
||||
this.messagingService.send("logout");
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async submitOld() {
|
||||
if (
|
||||
this.masterPasswordHint != null &&
|
||||
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
|
||||
@@ -242,6 +373,6 @@ export class ChangePasswordComponent
|
||||
|
||||
private async updateKey() {
|
||||
const user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedData(this.masterPassword, user);
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedDataLegacy(this.masterPassword, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export class AccountKeysRequest {
|
||||
// Other keys encrypted by the userkey
|
||||
userKeyEncryptedAccountPrivateKey: string;
|
||||
accountPublicKey: string;
|
||||
|
||||
constructor(userKeyEncryptedAccountPrivateKey: string, accountPublicKey: string) {
|
||||
this.userKeyEncryptedAccountPrivateKey = userKeyEncryptedAccountPrivateKey;
|
||||
this.accountPublicKey = accountPublicKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Argon2KdfConfig, KdfConfig, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
export class MasterPasswordUnlockDataRequest {
|
||||
kdfType: KdfType = KdfType.PBKDF2_SHA256;
|
||||
kdfIterations: number = 0;
|
||||
kdfMemory?: number;
|
||||
kdfParallelism?: number;
|
||||
|
||||
email: string;
|
||||
masterKeyAuthenticationHash: string;
|
||||
|
||||
masterKeyEncryptedUserKey: string;
|
||||
|
||||
masterPasswordHint?: string;
|
||||
|
||||
constructor(
|
||||
kdfConfig: KdfConfig,
|
||||
email: string,
|
||||
masterKeyAuthenticationHash: string,
|
||||
masterKeyEncryptedUserKey: string,
|
||||
masterPasswordHash?: string,
|
||||
) {
|
||||
this.kdfType = kdfConfig.kdfType;
|
||||
this.kdfIterations = kdfConfig.iterations;
|
||||
if (kdfConfig.kdfType === KdfType.Argon2id) {
|
||||
this.kdfMemory = (kdfConfig as Argon2KdfConfig).memory;
|
||||
this.kdfParallelism = (kdfConfig as Argon2KdfConfig).parallelism;
|
||||
}
|
||||
|
||||
this.email = email;
|
||||
this.masterKeyAuthenticationHash = masterKeyAuthenticationHash;
|
||||
this.masterKeyEncryptedUserKey = masterKeyEncryptedUserKey;
|
||||
this.masterPasswordHint = masterPasswordHash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AccountKeysRequest } from "./account-keys.request";
|
||||
import { UnlockDataRequest } from "./unlock-data.request";
|
||||
import { UserDataRequest as AccountDataRequest } from "./userdata.request";
|
||||
|
||||
export class RotateUserAccountKeysRequest {
|
||||
constructor(
|
||||
accountUnlockData: UnlockDataRequest,
|
||||
accountKeys: AccountKeysRequest,
|
||||
accountData: AccountDataRequest,
|
||||
oldMasterKeyAuthenticationHash: string,
|
||||
) {
|
||||
this.accountUnlockData = accountUnlockData;
|
||||
this.accountKeys = accountKeys;
|
||||
this.accountData = accountData;
|
||||
this.oldMasterKeyAuthenticationHash = oldMasterKeyAuthenticationHash;
|
||||
}
|
||||
|
||||
// Authentication for the request
|
||||
oldMasterKeyAuthenticationHash: string;
|
||||
|
||||
// All methods to get to the userkey
|
||||
accountUnlockData: UnlockDataRequest;
|
||||
|
||||
// Other keys encrypted by the userkey
|
||||
accountKeys: AccountKeysRequest;
|
||||
|
||||
// User vault data encrypted by the userkey
|
||||
accountData: AccountDataRequest;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
|
||||
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
|
||||
|
||||
import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request";
|
||||
|
||||
import { MasterPasswordUnlockDataRequest } from "./master-password-unlock-data.request";
|
||||
|
||||
export class UnlockDataRequest {
|
||||
// All methods to get to the userkey
|
||||
masterPasswordUnlockData: MasterPasswordUnlockDataRequest;
|
||||
emergencyAccessUnlockData: EmergencyAccessWithIdRequest[];
|
||||
organizationAccountRecoveryUnlockData: OrganizationUserResetPasswordWithIdRequest[];
|
||||
passkeyUnlockData: WebauthnRotateCredentialRequest[];
|
||||
|
||||
constructor(
|
||||
masterPasswordUnlockData: MasterPasswordUnlockDataRequest,
|
||||
emergencyAccessUnlockData: EmergencyAccessWithIdRequest[],
|
||||
organizationAccountRecoveryUnlockData: OrganizationUserResetPasswordWithIdRequest[],
|
||||
passkeyUnlockData: WebauthnRotateCredentialRequest[],
|
||||
) {
|
||||
this.masterPasswordUnlockData = masterPasswordUnlockData;
|
||||
this.emergencyAccessUnlockData = emergencyAccessUnlockData;
|
||||
this.organizationAccountRecoveryUnlockData = organizationAccountRecoveryUnlockData;
|
||||
this.passkeyUnlockData = passkeyUnlockData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
|
||||
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
|
||||
|
||||
export class UserDataRequest {
|
||||
ciphers: CipherWithIdRequest[];
|
||||
folders: FolderWithIdRequest[];
|
||||
sends: SendWithIdRequest[];
|
||||
|
||||
constructor(
|
||||
ciphers: CipherWithIdRequest[],
|
||||
folders: FolderWithIdRequest[],
|
||||
sends: SendWithIdRequest[],
|
||||
) {
|
||||
this.ciphers = ciphers;
|
||||
this.folders = folders;
|
||||
this.sends = sends;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { inject, Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
||||
import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
|
||||
import { UpdateKeyRequest } from "./request/update-key.request";
|
||||
|
||||
@Injectable()
|
||||
@@ -11,4 +12,14 @@ export class UserKeyRotationApiService {
|
||||
postUserKeyUpdate(request: UpdateKeyRequest): Promise<any> {
|
||||
return this.apiService.send("POST", "/accounts/key", request, true, false);
|
||||
}
|
||||
|
||||
postUserKeyUpdateV2(request: RotateUserAccountKeysRequest): Promise<any> {
|
||||
return this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/key-management/rotate-user-account-keys",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,21 +6,25 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey, UserPrivateKey } from "@bitwarden/common/types/key";
|
||||
import { UserKey, UserPrivateKey, UserPublicKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
|
||||
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { WebauthnLoginAdminService } from "../../auth/core";
|
||||
@@ -47,6 +51,9 @@ describe("KeyRotationService", () => {
|
||||
let mockSyncService: MockProxy<SyncService>;
|
||||
let mockWebauthnLoginAdminService: MockProxy<WebauthnLoginAdminService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockVaultTimeoutService: MockProxy<VaultTimeoutService>;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
|
||||
const mockUser = {
|
||||
id: "mockUserId" as UserId,
|
||||
@@ -70,6 +77,9 @@ describe("KeyRotationService", () => {
|
||||
mockSyncService = mock<SyncService>();
|
||||
mockWebauthnLoginAdminService = mock<WebauthnLoginAdminService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockVaultTimeoutService = mock<VaultTimeoutService>();
|
||||
mockToastService = mock<ToastService>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
|
||||
keyRotationService = new UserKeyRotationService(
|
||||
mockUserVerificationService,
|
||||
@@ -85,6 +95,9 @@ describe("KeyRotationService", () => {
|
||||
mockSyncService,
|
||||
mockWebauthnLoginAdminService,
|
||||
mockLogService,
|
||||
mockVaultTimeoutService,
|
||||
mockToastService,
|
||||
mockI18nService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -94,6 +107,7 @@ describe("KeyRotationService", () => {
|
||||
|
||||
describe("rotateUserKeyAndEncryptedData", () => {
|
||||
let privateKey: BehaviorSubject<UserPrivateKey | null>;
|
||||
let keyPair: BehaviorSubject<{ privateKey: UserPrivateKey; publicKey: UserPublicKey }>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockKeyService.makeUserKey.mockResolvedValue([
|
||||
@@ -112,6 +126,8 @@ describe("KeyRotationService", () => {
|
||||
// Mock user verification
|
||||
mockUserVerificationService.verifyUserByMasterPassword.mockResolvedValue({
|
||||
masterKey: "mockMasterKey" as any,
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
email: "mockEmail",
|
||||
policyOptions: null,
|
||||
});
|
||||
|
||||
@@ -122,6 +138,12 @@ describe("KeyRotationService", () => {
|
||||
privateKey = new BehaviorSubject("mockPrivateKey" as any);
|
||||
mockKeyService.userPrivateKeyWithLegacySupport$.mockReturnValue(privateKey);
|
||||
|
||||
keyPair = new BehaviorSubject({
|
||||
privateKey: "mockPrivateKey",
|
||||
publicKey: "mockPublicKey",
|
||||
} as any);
|
||||
mockKeyService.userEncryptionKeyPair$.mockReturnValue(keyPair);
|
||||
|
||||
// Mock ciphers
|
||||
const mockCiphers = [createMockCipher("1", "Cipher 1"), createMockCipher("2", "Cipher 2")];
|
||||
mockCipherService.getRotatedData.mockResolvedValue(mockCiphers);
|
||||
@@ -147,8 +169,8 @@ describe("KeyRotationService", () => {
|
||||
mockWebauthnLoginAdminService.getRotatedData.mockResolvedValue(webauthn);
|
||||
});
|
||||
|
||||
it("rotates the user key and encrypted data", async () => {
|
||||
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser);
|
||||
it("rotates the user key and encrypted data legacy", async () => {
|
||||
await keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser);
|
||||
|
||||
expect(mockApiService.postUserKeyUpdate).toHaveBeenCalled();
|
||||
const arg = mockApiService.postUserKeyUpdate.mock.calls[0][0];
|
||||
@@ -162,9 +184,47 @@ describe("KeyRotationService", () => {
|
||||
expect(arg.webauthnKeys.length).toBe(2);
|
||||
});
|
||||
|
||||
it("rotates the user key and encrypted data", async () => {
|
||||
await keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockNewMasterPassword",
|
||||
mockUser,
|
||||
);
|
||||
|
||||
expect(mockApiService.postUserKeyUpdateV2).toHaveBeenCalled();
|
||||
const arg = mockApiService.postUserKeyUpdateV2.mock.calls[0][0];
|
||||
expect(arg.oldMasterKeyAuthenticationHash).toBe("mockMasterPasswordHash");
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.email).toBe("mockEmail");
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.kdfType).toBe(
|
||||
DEFAULT_KDF_CONFIG.kdfType,
|
||||
);
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.kdfIterations).toBe(
|
||||
DEFAULT_KDF_CONFIG.iterations,
|
||||
);
|
||||
expect(arg.accountKeys.accountPublicKey).toBe(Utils.fromUtf8ToB64("mockPublicKey"));
|
||||
expect(arg.accountKeys.userKeyEncryptedAccountPrivateKey).toBe("mockEncryptedData");
|
||||
expect(arg.accountData.ciphers.length).toBe(2);
|
||||
expect(arg.accountData.folders.length).toBe(2);
|
||||
expect(arg.accountData.sends.length).toBe(2);
|
||||
});
|
||||
|
||||
it("legacy throws if master password provided is falsey", async () => {
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throws if master password provided is falsey", async () => {
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData("", "", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if user key creation fails", async () => {
|
||||
mockKeyService.makeUserKey.mockResolvedValueOnce([null, null]);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -175,15 +235,41 @@ describe("KeyRotationService", () => {
|
||||
]);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if no private key is found", async () => {
|
||||
privateKey.next(null);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throws if no private key is found", async () => {
|
||||
privateKey.next(null);
|
||||
keyPair.next(null);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if master password is incorrect", async () => {
|
||||
mockUserVerificationService.verifyUserByMasterPassword.mockRejectedValueOnce(
|
||||
new Error("Invalid master password"),
|
||||
);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -193,15 +279,31 @@ describe("KeyRotationService", () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if server rotation fails", async () => {
|
||||
mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError"));
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedDataLegacy("mockMasterPassword", mockUser),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throws if server rotation fails", async () => {
|
||||
mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError"));
|
||||
mockApiService.postUserKeyUpdateV2.mockRejectedValueOnce(new Error("mockError"));
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ import { VerificationType } from "@bitwarden/common/auth/enums/verification-type
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -15,13 +19,19 @@ import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { WebauthnLoginAdminService } from "../../auth/core";
|
||||
import { EmergencyAccessService } from "../../auth/emergency-access";
|
||||
|
||||
import { AccountKeysRequest } from "./request/account-keys.request";
|
||||
import { MasterPasswordUnlockDataRequest } from "./request/master-password-unlock-data.request";
|
||||
import { RotateUserAccountKeysRequest } from "./request/rotate-user-account-keys.request";
|
||||
import { UnlockDataRequest } from "./request/unlock-data.request";
|
||||
import { UpdateKeyRequest } from "./request/update-key.request";
|
||||
import { UserDataRequest } from "./request/userdata.request";
|
||||
import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
|
||||
|
||||
@Injectable()
|
||||
@@ -40,14 +50,180 @@ export class UserKeyRotationService {
|
||||
private syncService: SyncService,
|
||||
private webauthnLoginAdminService: WebauthnLoginAdminService,
|
||||
private logService: LogService,
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new user key and re-encrypts all required data with the it.
|
||||
* @param masterPassword current master password (used for validation)
|
||||
* @param oldMasterPassword: The current master password
|
||||
* @param newMasterPassword: The new master password
|
||||
* @param user: The user account
|
||||
* @param newMasterPasswordHint: The hint for the new master password
|
||||
*/
|
||||
async rotateUserKeyAndEncryptedData(masterPassword: string, user: Account): Promise<void> {
|
||||
async rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
oldMasterPassword: string,
|
||||
newMasterPassword: string,
|
||||
user: Account,
|
||||
newMasterPasswordHint?: string,
|
||||
): Promise<void> {
|
||||
this.logService.info("[Userkey rotation] Starting user key rotation...");
|
||||
if (!newMasterPassword) {
|
||||
this.logService.info("[Userkey rotation] Invalid master password provided. Aborting!");
|
||||
throw new Error("Invalid master password");
|
||||
}
|
||||
|
||||
if ((await this.syncService.getLastSync()) === null) {
|
||||
this.logService.info("[Userkey rotation] Client was never synced. Aborting!");
|
||||
throw new Error(
|
||||
"The local vault is de-synced and the keys cannot be rotated. Please log out and log back in to resolve this issue.",
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
masterKey: oldMasterKey,
|
||||
email,
|
||||
kdfConfig,
|
||||
} = await this.userVerificationService.verifyUserByMasterPassword(
|
||||
{
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: oldMasterPassword,
|
||||
},
|
||||
user.id,
|
||||
user.email,
|
||||
);
|
||||
|
||||
const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
|
||||
|
||||
const [newUnencryptedUserKey, newMasterKeyEncryptedUserKey] =
|
||||
await this.keyService.makeUserKey(newMasterKey);
|
||||
|
||||
if (!newUnencryptedUserKey || !newMasterKeyEncryptedUserKey) {
|
||||
this.logService.info("[Userkey rotation] User key could not be created. Aborting!");
|
||||
throw new Error("User key could not be created");
|
||||
}
|
||||
|
||||
const newMasterKeyAuthenticationHash = await this.keyService.hashMasterKey(
|
||||
newMasterPassword,
|
||||
newMasterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
const masterPasswordUnlockData = new MasterPasswordUnlockDataRequest(
|
||||
kdfConfig,
|
||||
email,
|
||||
newMasterKeyAuthenticationHash,
|
||||
newMasterKeyEncryptedUserKey.encryptedString!,
|
||||
newMasterPasswordHint,
|
||||
);
|
||||
|
||||
const keyPair = await firstValueFrom(this.keyService.userEncryptionKeyPair$(user.id));
|
||||
if (keyPair == null) {
|
||||
this.logService.info("[Userkey rotation] Key pair is null. Aborting!");
|
||||
throw new Error("Key pair is null");
|
||||
}
|
||||
const { privateKey, publicKey } = keyPair;
|
||||
|
||||
const accountKeysRequest = new AccountKeysRequest(
|
||||
(await this.encryptService.encrypt(privateKey, newUnencryptedUserKey)).encryptedString!,
|
||||
Utils.fromBufferToB64(publicKey),
|
||||
);
|
||||
|
||||
const originalUserKey = await firstValueFrom(this.keyService.userKey$(user.id));
|
||||
if (originalUserKey == null) {
|
||||
this.logService.info("[Userkey rotation] Userkey is null. Aborting!");
|
||||
throw new Error("Userkey key is null");
|
||||
}
|
||||
|
||||
const rotatedCiphers = await this.cipherService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
const rotatedFolders = await this.folderService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
const rotatedSends = await this.sendService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
if (rotatedCiphers == null || rotatedFolders == null || rotatedSends == null) {
|
||||
this.logService.info("[Userkey rotation] ciphers, folders, or sends are null. Aborting!");
|
||||
throw new Error("ciphers, folders, or sends are null");
|
||||
}
|
||||
const accountDataRequest = new UserDataRequest(rotatedCiphers, rotatedFolders, rotatedSends);
|
||||
|
||||
const emergencyAccessUnlockData = await this.emergencyAccessService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
// Note: Reset password keys request model has user verification
|
||||
// properties, but the rotation endpoint uses its own MP hash.
|
||||
const organizationAccountRecoveryUnlockData = await this.resetPasswordService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
if (organizationAccountRecoveryUnlockData == null) {
|
||||
this.logService.info(
|
||||
"[Userkey rotation] Organization account recovery data is null. Aborting!",
|
||||
);
|
||||
throw new Error("Organization account recovery data is null");
|
||||
}
|
||||
|
||||
const passkeyUnlockData = await this.webauthnLoginAdminService.getRotatedData(
|
||||
originalUserKey,
|
||||
newUnencryptedUserKey,
|
||||
user.id,
|
||||
);
|
||||
const unlockDataRequest = new UnlockDataRequest(
|
||||
masterPasswordUnlockData,
|
||||
emergencyAccessUnlockData,
|
||||
organizationAccountRecoveryUnlockData,
|
||||
passkeyUnlockData,
|
||||
);
|
||||
|
||||
const request = new RotateUserAccountKeysRequest(
|
||||
unlockDataRequest,
|
||||
accountKeysRequest,
|
||||
accountDataRequest,
|
||||
await this.keyService.hashMasterKey(oldMasterPassword, oldMasterKey),
|
||||
);
|
||||
|
||||
this.logService.info("[Userkey rotation] Posting user key rotation request to server");
|
||||
await this.apiService.postUserKeyUpdateV2(request);
|
||||
this.logService.info("[Userkey rotation] Userkey rotation request posted to server");
|
||||
|
||||
// TODO PM-2199: Add device trust rotation support to the user key rotation endpoint
|
||||
this.logService.info("[Userkey rotation] Rotating device trust...");
|
||||
await this.deviceTrustService.rotateDevicesTrust(
|
||||
user.id,
|
||||
newUnencryptedUserKey,
|
||||
newMasterKeyAuthenticationHash,
|
||||
);
|
||||
this.logService.info("[Userkey rotation] Device trust rotation completed");
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("rotationCompletedTitle"),
|
||||
message: this.i18nService.t("rotationCompletedDesc"),
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// temporary until userkey can be better verified
|
||||
await this.vaultTimeoutService.logOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user key and re-encrypts all required data with the it.
|
||||
* @param masterPassword current master password (used for validation)
|
||||
* @deprecated
|
||||
*/
|
||||
async rotateUserKeyAndEncryptedDataLegacy(masterPassword: string, user: Account): Promise<void> {
|
||||
this.logService.info("[Userkey rotation] Starting legacy user key rotation...");
|
||||
if (!masterPassword) {
|
||||
this.logService.info("[Userkey rotation] Invalid master password provided. Aborting!");
|
||||
throw new Error("Invalid master password");
|
||||
@@ -168,6 +344,7 @@ export class UserKeyRotationService {
|
||||
this.logService.info("[Userkey rotation] Rotating device trust...");
|
||||
await this.deviceTrustService.rotateDevicesTrust(user.id, newUserKey, masterPasswordHash);
|
||||
this.logService.info("[Userkey rotation] Device trust rotation completed");
|
||||
await this.vaultTimeoutService.logOut();
|
||||
}
|
||||
|
||||
private async encryptPrivateKey(
|
||||
|
||||
@@ -64,7 +64,7 @@ export class MigrateFromLegacyEncryptionComponent {
|
||||
try {
|
||||
await this.syncService.fullSync(false, true);
|
||||
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedData(masterPassword, activeUser);
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedDataLegacy(masterPassword, activeUser);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -224,6 +224,8 @@ describe("UserVerificationService", () => {
|
||||
expect(result).toEqual({
|
||||
policyOptions: null,
|
||||
masterKey: "masterKey",
|
||||
kdfConfig: "kdfConfig",
|
||||
email: "email",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,6 +284,8 @@ describe("UserVerificationService", () => {
|
||||
expect(result).toEqual({
|
||||
policyOptions: "MasterPasswordPolicyOptions",
|
||||
masterKey: "masterKey",
|
||||
kdfConfig: "kdfConfig",
|
||||
email: "email",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||
);
|
||||
await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
return { policyOptions, masterKey };
|
||||
return { policyOptions, masterKey, kdfConfig, email };
|
||||
}
|
||||
|
||||
private async verifyUserByPIN(verification: PinVerification, userId: UserId): Promise<boolean> {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { MasterKey } from "../../types/key";
|
||||
import { VerificationType } from "../enums/verification-type";
|
||||
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
|
||||
@@ -22,5 +24,7 @@ export type ServerSideVerification = OtpVerification | MasterPasswordVerificatio
|
||||
|
||||
export type MasterPasswordVerificationResponse = {
|
||||
masterKey: MasterKey;
|
||||
kdfConfig: KdfConfig;
|
||||
email: string;
|
||||
policyOptions: MasterPasswordPolicyResponse | null;
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ export enum FeatureFlag {
|
||||
/* Auth */
|
||||
PM9112_DeviceApprovalPersistence = "pm-9112-device-approval-persistence",
|
||||
|
||||
UserKeyRotationV2 = "userkey-rotation-v2",
|
||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
@@ -99,6 +100,7 @@ export const DefaultFeatureFlagValue = {
|
||||
/* Auth */
|
||||
[FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE,
|
||||
|
||||
[FeatureFlag.UserKeyRotationV2]: FALSE,
|
||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
|
||||
@@ -326,6 +326,17 @@ export abstract class KeyService {
|
||||
*/
|
||||
abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey | null>;
|
||||
|
||||
/**
|
||||
* Gets an observable stream of the given users decrypted private key and public key, guaranteed to be consistent.
|
||||
* Will emit null if the user doesn't have a userkey to decrypt the encrypted private key, or null if the user doesn't have a private key
|
||||
* at all.
|
||||
*
|
||||
* @param userId The user id of the user to get the data for.
|
||||
*/
|
||||
abstract userEncryptionKeyPair$(
|
||||
userId: UserId,
|
||||
): Observable<{ privateKey: UserPrivateKey; publicKey: UserPublicKey } | null>;
|
||||
|
||||
/**
|
||||
* Generates a fingerprint phrase for the user based on their public key
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs";
|
||||
import { BehaviorSubject, bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data";
|
||||
@@ -802,4 +802,51 @@ describe("keyService", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("userPrivateKey$", () => {
|
||||
type SetupKeysParams = {
|
||||
makeMasterKey: boolean;
|
||||
makeUserKey: boolean;
|
||||
};
|
||||
|
||||
function setupKeys({ makeMasterKey, makeUserKey }: SetupKeysParams): [UserKey, MasterKey] {
|
||||
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
|
||||
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null;
|
||||
masterPasswordService.masterKeySubject.next(fakeMasterKey);
|
||||
userKeyState.nextState(null);
|
||||
const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey<UserKey>(64) : null;
|
||||
userKeyState.nextState(fakeUserKey);
|
||||
return [fakeUserKey, fakeMasterKey];
|
||||
}
|
||||
|
||||
it("returns null when private key is null", async () => {
|
||||
setupKeys({ makeMasterKey: false, makeUserKey: false });
|
||||
|
||||
keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject(null));
|
||||
const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId));
|
||||
expect(key).toEqual(null);
|
||||
});
|
||||
|
||||
it("returns null when private key is undefined", async () => {
|
||||
setupKeys({ makeUserKey: true, makeMasterKey: false });
|
||||
|
||||
keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject(undefined));
|
||||
const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId));
|
||||
expect(key).toEqual(null);
|
||||
});
|
||||
|
||||
it("returns keys when private key is defined", async () => {
|
||||
setupKeys({ makeUserKey: false, makeMasterKey: true });
|
||||
|
||||
keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject("private key"));
|
||||
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(
|
||||
Utils.fromUtf8ToArray("public key"),
|
||||
);
|
||||
const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId));
|
||||
expect(key).toEqual({
|
||||
privateKey: "private key",
|
||||
publicKey: Utils.fromUtf8ToArray("public key"),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -895,6 +895,21 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
userEncryptionKeyPair$(
|
||||
userId: UserId,
|
||||
): Observable<{ privateKey: UserPrivateKey; publicKey: UserPublicKey } | null> {
|
||||
return this.userPrivateKey$(userId).pipe(
|
||||
switchMap(async (privateKey) => {
|
||||
if (privateKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publicKey = (await this.derivePublicKey(privateKey))!;
|
||||
return { privateKey, publicKey };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString | null> {
|
||||
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user