1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

[PM-14445] TS strict for Key Management, Keys and Lock component (#13121)

* PM-14445: TS strict for Key Management Biometrics

* formatting

* callbacks not null expectations

* state nullability expectations updates

* unit tests fix

* secure channel naming, explicit null check on messageId

* KM-14445: TS strict for Key Management, Keys and Lock component

* conflicts resolution, new strict check failures

* null simplifications

* migrate legacy encryption when no active user throw error instead of hiding it

* throw instead of return
This commit is contained in:
Maciej Zieniuk
2025-02-20 18:45:37 +01:00
committed by GitHub
parent ca41ecba29
commit 3924bc9c84
29 changed files with 403 additions and 279 deletions

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core"; import { inject } from "@angular/core";
import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs"; import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs";
@@ -26,7 +24,7 @@ export class ExtensionLockComponentService implements LockComponentService {
private readonly biometricStateService = inject(BiometricStateService); private readonly biometricStateService = inject(BiometricStateService);
getPreviousUrl(): string | null { getPreviousUrl(): string | null {
return this.routerService.getPreviousUrl(); return this.routerService.getPreviousUrl() ?? null;
} }
getBiometricsError(error: any): string | null { getBiometricsError(error: any): string | null {
@@ -71,7 +69,7 @@ export class ExtensionLockComponentService implements LockComponentService {
map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => { map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => {
const unlockOpts: UnlockOptions = { const unlockOpts: UnlockOptions = {
masterPassword: { masterPassword: {
enabled: userDecryptionOptions.hasMasterPassword, enabled: userDecryptionOptions?.hasMasterPassword,
}, },
pin: { pin: {
enabled: pinDecryptionAvailable, enabled: pinDecryptionAvailable,

View File

@@ -54,7 +54,7 @@ export class DesktopLockComponentService implements LockComponentService {
map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => { map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => {
const unlockOpts: UnlockOptions = { const unlockOpts: UnlockOptions = {
masterPassword: { masterPassword: {
enabled: userDecryptionOptions.hasMasterPassword, enabled: userDecryptionOptions?.hasMasterPassword,
}, },
pin: { pin: {
enabled: pinDecryptionAvailable, enabled: pinDecryptionAvailable,

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@@ -75,26 +73,23 @@ export class ElectronKeyService extends DefaultKeyService {
protected override async getKeyFromStorage( protected override async getKeyFromStorage(
keySuffix: KeySuffixOptions, keySuffix: KeySuffixOptions,
userId?: UserId, userId?: UserId,
): Promise<UserKey> { ): Promise<UserKey | null> {
return await super.getKeyFromStorage(keySuffix, userId); return await super.getKeyFromStorage(keySuffix, userId);
} }
protected async storeBiometricsProtectedUserKey( private async storeBiometricsProtectedUserKey(userKey: UserKey, userId: UserId): Promise<void> {
userKey: UserKey,
userId?: UserId,
): Promise<void> {
// May resolve to null, in which case no client key have is required // May resolve to null, in which case no client key have is required
// TODO: Move to windows implementation // TODO: Move to windows implementation
const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userKey, userId); const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userKey, userId);
await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf); await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf as string);
await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64); await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64);
} }
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> { protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId): Promise<boolean> {
return await super.shouldStoreKey(keySuffix, userId); return await super.shouldStoreKey(keySuffix, userId);
} }
protected override async clearAllStoredUserKeys(userId?: UserId): Promise<void> { protected override async clearAllStoredUserKeys(userId: UserId): Promise<void> {
await this.biometricService.deleteBiometricUnlockKeyForUser(userId); await this.biometricService.deleteBiometricUnlockKeyForUser(userId);
await super.clearAllStoredUserKeys(userId); await super.clearAllStoredUserKeys(userId);
} }

View File

@@ -56,6 +56,9 @@ export class RotateableKeySetService {
keySet.encryptedPublicKey, keySet.encryptedPublicKey,
oldUserKey, oldUserKey,
); );
if (publicKey == null) {
throw new Error("failed to rotate key set: could not decrypt public key");
}
const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey); const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey);
const newEncryptedUserKey = await this.encryptService.rsaEncrypt(newUserKey.key, publicKey); const newEncryptedUserKey = await this.encryptService.rsaEncrypt(newUserKey.key, publicKey);

View File

@@ -1,16 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
// FIXME: remove `src` and fix import import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
// eslint-disable-next-line no-restricted-imports import { CipherWithIdRequest } from "@bitwarden/common/vault/models/request/cipher-with-id.request";
import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { FolderWithIdRequest } from "@bitwarden/common/src/vault/models/request/folder-with-id.request";
import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request"; import { EmergencyAccessWithIdRequest } from "../../../auth/emergency-access/request/emergency-access-update.request";
@@ -24,4 +16,10 @@ export class UpdateKeyRequest {
emergencyAccessKeys: EmergencyAccessWithIdRequest[] = []; emergencyAccessKeys: EmergencyAccessWithIdRequest[] = [];
resetPasswordKeys: OrganizationUserResetPasswordWithIdRequest[] = []; resetPasswordKeys: OrganizationUserResetPasswordWithIdRequest[] = [];
webauthnKeys: WebauthnRotateCredentialRequest[] = []; webauthnKeys: WebauthnRotateCredentialRequest[] = [];
constructor(masterPasswordHash: string, key: string, privateKey: string) {
this.masterPasswordHash = masterPasswordHash;
this.key = key;
this.privateKey = privateKey;
}
} }

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
@@ -10,6 +8,7 @@ import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/r
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; 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 { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
@@ -24,9 +23,9 @@ import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/fold
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
import { WebauthnLoginAdminService } from "../core"; import { WebauthnLoginAdminService } from "../../auth/core";
import { EmergencyAccessService } from "../emergency-access"; import { EmergencyAccessService } from "../../auth/emergency-access";
import { EmergencyAccessWithIdRequest } from "../emergency-access/request/emergency-access-update.request"; import { EmergencyAccessWithIdRequest } from "../../auth/emergency-access/request/emergency-access-update.request";
import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
import { UserKeyRotationService } from "./user-key-rotation.service"; import { UserKeyRotationService } from "./user-key-rotation.service";
@@ -94,7 +93,7 @@ describe("KeyRotationService", () => {
}); });
describe("rotateUserKeyAndEncryptedData", () => { describe("rotateUserKeyAndEncryptedData", () => {
let privateKey: BehaviorSubject<UserPrivateKey>; let privateKey: BehaviorSubject<UserPrivateKey | null>;
beforeEach(() => { beforeEach(() => {
mockKeyService.makeUserKey.mockResolvedValue([ mockKeyService.makeUserKey.mockResolvedValue([
@@ -170,7 +169,10 @@ describe("KeyRotationService", () => {
}); });
it("throws if user key creation fails", async () => { it("throws if user key creation fails", async () => {
mockKeyService.makeUserKey.mockResolvedValueOnce([null, null]); mockKeyService.makeUserKey.mockResolvedValueOnce([
null as unknown as UserKey,
null as unknown as EncString,
]);
await expect( await expect(
keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser), keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser),

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
@@ -77,20 +75,16 @@ export class UserKeyRotationService {
const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(masterKey); const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(masterKey);
if (!newUserKey || !newEncUserKey) { if (newUserKey == null || newEncUserKey == null || newEncUserKey.encryptedString == null) {
this.logService.info("[Userkey rotation] User key could not be created. Aborting!"); this.logService.info("[Userkey rotation] User key could not be created. Aborting!");
throw new Error("User key could not be created"); throw new Error("User key could not be created");
} }
// Create new request // New user key
const request = new UpdateKeyRequest(); const key = newEncUserKey.encryptedString;
// Add new user key
request.key = newEncUserKey.encryptedString;
// Add master key hash // Add master key hash
const masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey); const masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey);
request.masterPasswordHash = masterPasswordHash;
// Get original user key // Get original user key
// Note: We distribute the legacy key, but not all domains actually use it. If any of those // Note: We distribute the legacy key, but not all domains actually use it. If any of those
@@ -101,7 +95,14 @@ export class UserKeyRotationService {
this.logService.info("[Userkey rotation] Is legacy user: " + isMasterKey); this.logService.info("[Userkey rotation] Is legacy user: " + isMasterKey);
// Add re-encrypted data // Add re-encrypted data
request.privateKey = await this.encryptPrivateKey(newUserKey, user.id); const privateKey = await this.encryptPrivateKey(newUserKey, user.id);
if (privateKey == null) {
this.logService.info("[Userkey rotation] Private key could not be encrypted. Aborting!");
throw new Error("Private key could not be encrypted");
}
// Create new request
const request = new UpdateKeyRequest(masterPasswordHash, key, privateKey);
const rotatedCiphers = await this.cipherService.getRotatedData( const rotatedCiphers = await this.cipherService.getRotatedData(
originalUserKey, originalUserKey,
@@ -172,11 +173,11 @@ export class UserKeyRotationService {
private async encryptPrivateKey( private async encryptPrivateKey(
newUserKey: UserKey, newUserKey: UserKey,
userId: UserId, userId: UserId,
): Promise<EncryptedString | null> { ): Promise<EncryptedString | undefined> {
const privateKey = await firstValueFrom( const privateKey = await firstValueFrom(
this.keyService.userPrivateKeyWithLegacySupport$(userId), this.keyService.userPrivateKeyWithLegacySupport$(userId),
); );
if (!privateKey) { if (privateKey == null) {
throw new Error("No private key found for user key rotation"); throw new Error("No private key found for user key rotation");
} }
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString; return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;

View File

@@ -34,8 +34,8 @@ export class WebLockComponentService implements LockComponentService {
); );
} }
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> { getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions | null> {
return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe( return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId)?.pipe(
map((userDecryptionOptions: UserDecryptionOptions) => { map((userDecryptionOptions: UserDecryptionOptions) => {
const unlockOpts: UnlockOptions = { const unlockOpts: UnlockOptions = {
masterPassword: { masterPassword: {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms"; import { FormControl, FormGroup, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
@@ -50,6 +48,9 @@ export class MigrateFromLegacyEncryptionComponent {
} }
const activeUser = await firstValueFrom(this.accountService.activeAccount$); const activeUser = await firstValueFrom(this.accountService.activeAccount$);
if (activeUser == null) {
throw new Error("No active user.");
}
const hasUserKey = await this.keyService.hasUserKey(activeUser.id); const hasUserKey = await this.keyService.hasUserKey(activeUser.id);
if (hasUserKey) { if (hasUserKey) {
@@ -57,7 +58,7 @@ export class MigrateFromLegacyEncryptionComponent {
throw new Error("User key already exists, cannot migrate legacy encryption."); throw new Error("User key already exists, cannot migrate legacy encryption.");
} }
const masterPassword = this.formGroup.value.masterPassword; const masterPassword = this.formGroup.value.masterPassword!;
try { try {
await this.syncService.fullSync(false, true); await this.syncService.fullSync(false, true);
@@ -73,7 +74,10 @@ export class MigrateFromLegacyEncryptionComponent {
this.messagingService.send("logout"); this.messagingService.send("logout");
} catch (e) { } catch (e) {
// If the error is due to missing folders, we can delete all folders and try again // If the error is due to missing folders, we can delete all folders and try again
if (e.message === "All existing folders must be included in the rotation.") { if (
e instanceof Error &&
e.message === "All existing folders must be included in the rotation."
) {
const deleteFolders = await this.dialogService.openSimpleDialog({ const deleteFolders = await this.dialogService.openSimpleDialog({
type: "warning", type: "warning",
title: { key: "encryptionKeyUpdateCannotProceed" }, title: { key: "encryptionKeyUpdateCannotProceed" },

View File

@@ -35,6 +35,10 @@ describe("CriticalAppsService", () => {
// reset mocks // reset mocks
jest.resetAllMocks(); jest.resetAllMocks();
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
keyService.getOrgKey.mockResolvedValue(mockOrgKey);
}); });
it("should be created", () => { it("should be created", () => {

View File

@@ -62,6 +62,9 @@ export class CriticalAppsService {
// Save the selected critical apps for a given organization // Save the selected critical apps for a given organization
async setCriticalApps(orgId: string, selectedUrls: string[]) { async setCriticalApps(orgId: string, selectedUrls: string[]) {
const key = await this.keyService.getOrgKey(orgId); const key = await this.keyService.getOrgKey(orgId);
if (key == null) {
throw new Error("Organization key not found");
}
// only save records that are not already in the database // only save records that are not already in the database
const newEntries = await this.filterNewEntries(orgId as OrganizationId, selectedUrls); const newEntries = await this.filterNewEntries(orgId as OrganizationId, selectedUrls);
@@ -129,6 +132,10 @@ export class CriticalAppsService {
from(this.keyService.getOrgKey(orgId)), from(this.keyService.getOrgKey(orgId)),
).pipe( ).pipe(
switchMap(([response, key]) => { switchMap(([response, key]) => {
if (key == null) {
throw new Error("Organization key not found");
}
const results = response.map(async (r: PasswordHealthReportApplicationsResponse) => { const results = response.map(async (r: PasswordHealthReportApplicationsResponse) => {
const encrypted = new EncString(r.uri); const encrypted = new EncString(r.uri);
const uri = await this.encryptService.decryptToUtf8(encrypted, key); const uri = await this.encryptService.decryptToUtf8(encrypted, key);

View File

@@ -30,7 +30,7 @@ export abstract class PinServiceAbstraction {
/** /**
* Gets the persistent (stored on disk) version of the UserKey, encrypted by the PinKey. * Gets the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
*/ */
abstract getPinKeyEncryptedUserKeyPersistent: (userId: UserId) => Promise<EncString>; abstract getPinKeyEncryptedUserKeyPersistent: (userId: UserId) => Promise<EncString | null>;
/** /**
* Clears the persistent (stored on disk) version of the UserKey, encrypted by the PinKey. * Clears the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
@@ -40,7 +40,7 @@ export abstract class PinServiceAbstraction {
/** /**
* Gets the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey. * Gets the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
*/ */
abstract getPinKeyEncryptedUserKeyEphemeral: (userId: UserId) => Promise<EncString>; abstract getPinKeyEncryptedUserKeyEphemeral: (userId: UserId) => Promise<EncString | null>;
/** /**
* Clears the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey. * Clears the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
@@ -70,7 +70,7 @@ export abstract class PinServiceAbstraction {
/** /**
* Gets the user's PIN, encrypted by the UserKey. * Gets the user's PIN, encrypted by the UserKey.
*/ */
abstract getUserKeyEncryptedPin: (userId: UserId) => Promise<EncString>; abstract getUserKeyEncryptedPin: (userId: UserId) => Promise<EncString | null>;
/** /**
* Sets the user's PIN, encrypted by the UserKey. * Sets the user's PIN, encrypted by the UserKey.
@@ -94,7 +94,7 @@ export abstract class PinServiceAbstraction {
* Gets the old MasterKey, encrypted by the PinKey (formerly called `pinProtected`). * Gets the old MasterKey, encrypted by the PinKey (formerly called `pinProtected`).
* Deprecated and used for migration purposes only. * Deprecated and used for migration purposes only.
*/ */
abstract getOldPinKeyEncryptedMasterKey: (userId: UserId) => Promise<EncryptedString>; abstract getOldPinKeyEncryptedMasterKey: (userId: UserId) => Promise<EncryptedString | null>;
/** /**
* Clears the old MasterKey, encrypted by the PinKey. * Clears the old MasterKey, encrypted by the PinKey.

View File

@@ -99,7 +99,7 @@ export class PinService implements PinServiceAbstraction {
private stateService: StateService, private stateService: StateService,
) {} ) {}
async getPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString> { async getPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString | null> {
this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyPersistent."); this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyPersistent.");
return EncString.fromJSON( return EncString.fromJSON(
@@ -137,7 +137,7 @@ export class PinService implements PinServiceAbstraction {
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, userId); await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, userId);
} }
async getPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<EncString> { async getPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<EncString | null> {
this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyEphemeral."); this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyEphemeral.");
return EncString.fromJSON( return EncString.fromJSON(
@@ -210,7 +210,7 @@ export class PinService implements PinServiceAbstraction {
} }
} }
async getUserKeyEncryptedPin(userId: UserId): Promise<EncString> { async getUserKeyEncryptedPin(userId: UserId): Promise<EncString | null> {
this.validateUserId(userId, "Cannot get userKeyEncryptedPin."); this.validateUserId(userId, "Cannot get userKeyEncryptedPin.");
return EncString.fromJSON( return EncString.fromJSON(
@@ -242,7 +242,7 @@ export class PinService implements PinServiceAbstraction {
return await this.encryptService.encrypt(pin, userKey); return await this.encryptService.encrypt(pin, userKey);
} }
async getOldPinKeyEncryptedMasterKey(userId: UserId): Promise<EncryptedString> { async getOldPinKeyEncryptedMasterKey(userId: UserId): Promise<EncryptedString | null> {
this.validateUserId(userId, "Cannot get oldPinKeyEncryptedMasterKey."); this.validateUserId(userId, "Cannot get oldPinKeyEncryptedMasterKey.");
return await firstValueFrom( return await firstValueFrom(

View File

@@ -123,7 +123,7 @@ export class FakeSingleUserState<T> implements SingleUserState<T> {
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state)); this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
} }
nextState(state: T, { syncValue }: { syncValue: boolean } = { syncValue: true }) { nextState(state: T | null, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
this.stateSubject.next({ this.stateSubject.next({
syncValue, syncValue,
combinedState: [this.userId, state], combinedState: [this.userId, state],
@@ -198,7 +198,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
return this.accountService.activeUserId; return this.accountService.activeUserId;
} }
nextState(state: T, { syncValue }: { syncValue: boolean } = { syncValue: true }) { nextState(state: T | null, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
this.stateSubject.next({ this.stateSubject.next({
syncValue, syncValue,
combinedState: [this.userId, state], combinedState: [this.userId, state],

View File

@@ -58,6 +58,9 @@ export class ProviderEncryptedOrganizationKey implements BaseEncryptedOrganizati
new EncString(this.key), new EncString(this.key),
providerKeys[this.providerId], providerKeys[this.providerId],
); );
if (decValue == null) {
throw new Error("Failed to decrypt organization key");
}
return new SymmetricCryptoKey(decValue) as OrgKey; return new SymmetricCryptoKey(decValue) as OrgKey;
} }

View File

@@ -13,9 +13,9 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
mock = mock<InternalMasterPasswordServiceAbstraction>(); mock = mock<InternalMasterPasswordServiceAbstraction>();
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class // eslint-disable-next-line rxjs/no-exposed-subjects -- test class
masterKeySubject = new ReplaySubject<MasterKey>(1); masterKeySubject = new ReplaySubject<MasterKey | null>(1);
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class // eslint-disable-next-line rxjs/no-exposed-subjects -- test class
masterKeyHashSubject = new ReplaySubject<string>(1); masterKeyHashSubject = new ReplaySubject<string | null>(1);
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class // eslint-disable-next-line rxjs/no-exposed-subjects -- test class
forceSetPasswordReasonSubject = new ReplaySubject<ForceSetPasswordReason>(1); forceSetPasswordReasonSubject = new ReplaySubject<ForceSetPasswordReason>(1);

View File

@@ -22,5 +22,5 @@ export type ServerSideVerification = OtpVerification | MasterPasswordVerificatio
export type MasterPasswordVerificationResponse = { export type MasterPasswordVerificationResponse = {
masterKey: MasterKey; masterKey: MasterKey;
policyOptions: MasterPasswordPolicyResponse; policyOptions: MasterPasswordPolicyResponse | null;
}; };

View File

@@ -33,7 +33,7 @@ export abstract class EncryptService {
encThing: Encrypted, encThing: Encrypted,
key: SymmetricCryptoKey, key: SymmetricCryptoKey,
decryptTrace?: string, decryptTrace?: string,
): Promise<Uint8Array>; ): Promise<Uint8Array | null>;
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;

View File

@@ -136,7 +136,7 @@ export class EncryptServiceImplementation implements EncryptService {
encThing: Encrypted, encThing: Encrypted,
key: SymmetricCryptoKey, key: SymmetricCryptoKey,
decryptContext: string = "no context", decryptContext: string = "no context",
): Promise<Uint8Array> { ): Promise<Uint8Array | null> {
if (key == null) { if (key == null) {
throw new Error("No encryption key provided."); throw new Error("No encryption key provided.");
} }

View File

@@ -30,7 +30,7 @@ export abstract class StateService<T extends Account = Account> {
/** /**
* Sets the user's auto key * Sets the user's auto key
*/ */
setUserKeyAutoUnlock: (value: string, options?: StorageOptions) => Promise<void>; setUserKeyAutoUnlock: (value: string | null, options?: StorageOptions) => Promise<void>;
/** /**
* Gets the user's biometric key * Gets the user's biometric key
*/ */
@@ -57,7 +57,7 @@ export abstract class StateService<T extends Account = Account> {
/** /**
* @deprecated For migration purposes only, use setUserKeyAuto instead * @deprecated For migration purposes only, use setUserKeyAuto instead
*/ */
setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise<void>; setCryptoMasterKeyAuto: (value: string | null, options?: StorageOptions) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;

View File

@@ -8,7 +8,7 @@ import { ObservableInput, OperatorFunction, map } from "rxjs";
*/ */
export function convertValues<TKey extends PropertyKey, TInput, TOutput>( export function convertValues<TKey extends PropertyKey, TInput, TOutput>(
project: (key: TKey, value: TInput) => ObservableInput<TOutput>, project: (key: TKey, value: TInput) => ObservableInput<TOutput>,
): OperatorFunction<Record<TKey, TInput>, Record<TKey, ObservableInput<TOutput>>> { ): OperatorFunction<Record<TKey, TInput> | null, Record<TKey, ObservableInput<TOutput>>> {
return map((inputRecord) => { return map((inputRecord) => {
if (inputRecord == null) { if (inputRecord == null) {
return null; return null;

View File

@@ -158,7 +158,11 @@ export class EncString implements Encrypted {
return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length; return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length;
} }
async decrypt(orgId: string, key: SymmetricCryptoKey = null, context?: string): Promise<string> { async decrypt(
orgId: string | null,
key: SymmetricCryptoKey = null,
context?: string,
): Promise<string> {
if (this.decryptedValue != null) { if (this.decryptedValue != null) {
return this.decryptedValue; return this.decryptedValue;
} }

View File

@@ -170,7 +170,7 @@ export class StateService<
/** /**
* user key when using the "never" option of vault timeout * user key when using the "never" option of vault timeout
*/ */
async setUserKeyAutoUnlock(value: string, options?: StorageOptions): Promise<void> { async setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions( options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }), this.reconcileOptions(options, { keySuffix: "auto" }),
await this.defaultSecureStorageOptions(), await this.defaultSecureStorageOptions(),
@@ -226,7 +226,7 @@ export class StateService<
/** /**
* @deprecated Use UserKeyAuto instead * @deprecated Use UserKeyAuto instead
*/ */
async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise<void> { async setCryptoMasterKeyAuto(value: string | null, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions( options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }), this.reconcileOptions(options, { keySuffix: "auto" }),
await this.defaultSecureStorageOptions(), await this.defaultSecureStorageOptions(),
@@ -663,7 +663,7 @@ export class StateService<
protected async saveSecureStorageKey<T extends JsonValue>( protected async saveSecureStorageKey<T extends JsonValue>(
key: string, key: string,
value: T, value: T | null,
options?: StorageOptions, options?: StorageOptions,
) { ) {
return value == null return value == null

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
@@ -90,42 +88,41 @@ const AUTOPROMPT_BIOMETRICS_PROCESS_RELOAD_DELAY = 5000;
export class LockComponent implements OnInit, OnDestroy { export class LockComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
activeAccount: Account | null; activeAccount: Account | null = null;
clientType: ClientType; clientType?: ClientType;
ClientType = ClientType;
unlockOptions: UnlockOptions = null; unlockOptions: UnlockOptions | null = null;
UnlockOption = UnlockOption; UnlockOption = UnlockOption;
private _activeUnlockOptionBSubject: BehaviorSubject<UnlockOptionValue> = private _activeUnlockOptionBSubject: BehaviorSubject<UnlockOptionValue | null> =
new BehaviorSubject<UnlockOptionValue>(null); new BehaviorSubject<UnlockOptionValue | null>(null);
activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable(); activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable();
set activeUnlockOption(value: UnlockOptionValue) { set activeUnlockOption(value: UnlockOptionValue | null) {
this._activeUnlockOptionBSubject.next(value); this._activeUnlockOptionBSubject.next(value);
} }
get activeUnlockOption(): UnlockOptionValue { get activeUnlockOption(): UnlockOptionValue | null {
return this._activeUnlockOptionBSubject.value; return this._activeUnlockOptionBSubject.value;
} }
private invalidPinAttempts = 0; private invalidPinAttempts = 0;
biometricUnlockBtnText: string; biometricUnlockBtnText?: string;
// masterPassword = ""; // masterPassword = "";
showPassword = false; showPassword = false;
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined; private enforcedMasterPasswordOptions?: MasterPasswordPolicyOptions = undefined;
forcePasswordResetRoute = "update-temp-password"; forcePasswordResetRoute = "update-temp-password";
formGroup: FormGroup; formGroup: FormGroup | null = null;
// Desktop properties: // Desktop properties:
private deferFocus: boolean = null; private deferFocus: boolean | null = null;
private biometricAsked = false; private biometricAsked = false;
defaultUnlockOptionSetForUser = false; defaultUnlockOptionSetForUser = false;
@@ -174,7 +171,7 @@ export class LockComponent implements OnInit, OnDestroy {
// Identify client // Identify client
this.clientType = this.platformUtilsService.getClientType(); this.clientType = this.platformUtilsService.getClientType();
if (this.clientType === "desktop") { if (this.clientType === ClientType.Desktop) {
await this.desktopOnInit(); await this.desktopOnInit();
} else if (this.clientType === ClientType.Browser) { } else if (this.clientType === ClientType.Browser) {
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText(); this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
@@ -185,9 +182,11 @@ export class LockComponent implements OnInit, OnDestroy {
interval(1000) interval(1000)
.pipe( .pipe(
mergeMap(async () => { mergeMap(async () => {
this.unlockOptions = await firstValueFrom( if (this.activeAccount?.id != null) {
this.lockComponentService.getAvailableUnlockOptions$(this.activeAccount.id), this.unlockOptions = await firstValueFrom(
); this.lockComponentService.getAvailableUnlockOptions$(this.activeAccount.id),
);
}
}), }),
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
@@ -198,7 +197,7 @@ export class LockComponent implements OnInit, OnDestroy {
private listenForActiveUnlockOptionChanges() { private listenForActiveUnlockOptionChanges() {
this.activeUnlockOption$ this.activeUnlockOption$
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe((activeUnlockOption: UnlockOptionValue) => { .subscribe((activeUnlockOption: UnlockOptionValue | null) => {
if (activeUnlockOption === UnlockOption.Pin) { if (activeUnlockOption === UnlockOption.Pin) {
this.buildPinForm(); this.buildPinForm();
} else if (activeUnlockOption === UnlockOption.MasterPassword) { } else if (activeUnlockOption === UnlockOption.MasterPassword) {
@@ -257,7 +256,7 @@ export class LockComponent implements OnInit, OnDestroy {
this.setDefaultActiveUnlockOption(this.unlockOptions); this.setDefaultActiveUnlockOption(this.unlockOptions);
if (this.unlockOptions.biometrics.enabled) { if (this.unlockOptions?.biometrics.enabled) {
await this.handleBiometricsUnlockEnabled(); await this.handleBiometricsUnlockEnabled();
} }
} }
@@ -278,13 +277,13 @@ export class LockComponent implements OnInit, OnDestroy {
}); });
} }
private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions) { private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions | null) {
// Priorities should be Biometrics > Pin > Master Password for speed // Priorities should be Biometrics > Pin > Master Password for speed
if (unlockOptions.biometrics.enabled) { if (unlockOptions?.biometrics.enabled) {
this.activeUnlockOption = UnlockOption.Biometrics; this.activeUnlockOption = UnlockOption.Biometrics;
} else if (unlockOptions.pin.enabled) { } else if (unlockOptions?.pin.enabled) {
this.activeUnlockOption = UnlockOption.Pin; this.activeUnlockOption = UnlockOption.Pin;
} else if (unlockOptions.masterPassword.enabled) { } else if (unlockOptions?.masterPassword.enabled) {
this.activeUnlockOption = UnlockOption.MasterPassword; this.activeUnlockOption = UnlockOption.MasterPassword;
} }
} }
@@ -311,7 +310,7 @@ export class LockComponent implements OnInit, OnDestroy {
} }
if ( if (
this.unlockOptions.biometrics.enabled && this.unlockOptions?.biometrics.enabled &&
autoPromptBiometrics && autoPromptBiometrics &&
(await this.biometricService.getShouldAutopromptNow()) (await this.biometricService.getShouldAutopromptNow())
) { ) {
@@ -347,7 +346,7 @@ export class LockComponent implements OnInit, OnDestroy {
type: "warning", type: "warning",
}); });
if (confirmed) { if (confirmed && this.activeAccount != null) {
this.messagingService.send("logout", { userId: this.activeAccount.id }); this.messagingService.send("logout", { userId: this.activeAccount.id });
} }
} }
@@ -355,7 +354,11 @@ export class LockComponent implements OnInit, OnDestroy {
async unlockViaBiometrics(): Promise<void> { async unlockViaBiometrics(): Promise<void> {
this.unlockingViaBiometrics = true; this.unlockingViaBiometrics = true;
if (!this.unlockOptions.biometrics.enabled) { if (
this.unlockOptions == null ||
!this.unlockOptions.biometrics.enabled ||
this.activeAccount == null
) {
this.unlockingViaBiometrics = false; this.unlockingViaBiometrics = false;
return; return;
} }
@@ -374,7 +377,7 @@ export class LockComponent implements OnInit, OnDestroy {
this.unlockingViaBiometrics = false; this.unlockingViaBiometrics = false;
} catch (e) { } catch (e) {
// Cancelling is a valid action. // Cancelling is a valid action.
if (e?.message === "canceled") { if (e instanceof Error && e.message === "canceled") {
this.unlockingViaBiometrics = false; this.unlockingViaBiometrics = false;
return; return;
} }
@@ -413,8 +416,13 @@ export class LockComponent implements OnInit, OnDestroy {
togglePassword() { togglePassword() {
this.showPassword = !this.showPassword; this.showPassword = !this.showPassword;
const input = document.getElementById( const input = document.getElementById(
this.unlockOptions.pin.enabled ? "pin" : "masterPassword", this.unlockOptions?.pin.enabled ? "pin" : "masterPassword",
); );
if (input == null) {
return;
}
if (this.ngZone.isStable) { if (this.ngZone.isStable) {
input.focus(); input.focus();
} else { } else {
@@ -424,7 +432,7 @@ export class LockComponent implements OnInit, OnDestroy {
} }
private validatePin(): boolean { private validatePin(): boolean {
if (this.formGroup.invalid) { if (this.formGroup?.invalid) {
this.toastService.showToast({ this.toastService.showToast({
variant: "error", variant: "error",
title: this.i18nService.t("errorOccurred"), title: this.i18nService.t("errorOccurred"),
@@ -437,7 +445,7 @@ export class LockComponent implements OnInit, OnDestroy {
} }
private async unlockViaPin() { private async unlockViaPin() {
if (!this.validatePin()) { if (!this.validatePin() || this.formGroup == null || this.activeAccount == null) {
return; return;
} }
@@ -460,7 +468,6 @@ export class LockComponent implements OnInit, OnDestroy {
if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) { if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) {
this.toastService.showToast({ this.toastService.showToast({
variant: "error", variant: "error",
title: null,
message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
}); });
this.messagingService.send("logout"); this.messagingService.send("logout");
@@ -482,7 +489,7 @@ export class LockComponent implements OnInit, OnDestroy {
} }
private validateMasterPassword(): boolean { private validateMasterPassword(): boolean {
if (this.formGroup.invalid) { if (this.formGroup?.invalid) {
this.toastService.showToast({ this.toastService.showToast({
variant: "error", variant: "error",
title: this.i18nService.t("errorOccurred"), title: this.i18nService.t("errorOccurred"),
@@ -495,7 +502,7 @@ export class LockComponent implements OnInit, OnDestroy {
} }
private async unlockViaMasterPassword() { private async unlockViaMasterPassword() {
if (!this.validateMasterPassword()) { if (!this.validateMasterPassword() || this.formGroup == null || this.activeAccount == null) {
return; return;
} }
@@ -507,7 +514,7 @@ export class LockComponent implements OnInit, OnDestroy {
} as MasterPasswordVerification; } as MasterPasswordVerification;
let passwordValid = false; let passwordValid = false;
let masterPasswordVerificationResponse: MasterPasswordVerificationResponse; let masterPasswordVerificationResponse: MasterPasswordVerificationResponse | null = null;
try { try {
masterPasswordVerificationResponse = masterPasswordVerificationResponse =
await this.userVerificationService.verifyUserByMasterPassword( await this.userVerificationService.verifyUserByMasterPassword(
@@ -516,10 +523,12 @@ export class LockComponent implements OnInit, OnDestroy {
this.activeAccount.email, this.activeAccount.email,
); );
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse( if (masterPasswordVerificationResponse?.policyOptions != null) {
masterPasswordVerificationResponse.policyOptions, this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(
); masterPasswordVerificationResponse.policyOptions,
passwordValid = true; );
passwordValid = true;
}
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
@@ -534,13 +543,17 @@ export class LockComponent implements OnInit, OnDestroy {
} }
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterPasswordVerificationResponse.masterKey, masterPasswordVerificationResponse!.masterKey,
this.activeAccount.id, this.activeAccount.id,
); );
await this.setUserKeyAndContinue(userKey, true); await this.setUserKeyAndContinue(userKey, true);
} }
private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) { private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) {
if (this.activeAccount == null) {
throw new Error("No active user.");
}
await this.keyService.setUserKey(key, this.activeAccount.id); await this.keyService.setUserKey(key, this.activeAccount.id);
// Now that we have a decrypted user key in memory, we can check if we // Now that we have a decrypted user key in memory, we can check if we
@@ -551,10 +564,19 @@ export class LockComponent implements OnInit, OnDestroy {
} }
private async doContinue(evaluatePasswordAfterUnlock: boolean) { private async doContinue(evaluatePasswordAfterUnlock: boolean) {
if (this.activeAccount == null) {
throw new Error("No active user.");
}
await this.biometricStateService.resetUserPromptCancelled(); await this.biometricStateService.resetUserPromptCancelled();
this.messagingService.send("unlocked"); this.messagingService.send("unlocked");
if (evaluatePasswordAfterUnlock) { if (evaluatePasswordAfterUnlock) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (userId == null) {
throw new Error("No active user.");
}
try { try {
// If we do not have any saved policies, attempt to load them from the service // If we do not have any saved policies, attempt to load them from the service
if (this.enforcedMasterPasswordOptions == undefined) { if (this.enforcedMasterPasswordOptions == undefined) {
@@ -564,7 +586,6 @@ export class LockComponent implements OnInit, OnDestroy {
} }
if (this.requirePasswordChange()) { if (this.requirePasswordChange()) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason( await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.WeakMasterPassword, ForceSetPasswordReason.WeakMasterPassword,
userId, userId,
@@ -597,8 +618,10 @@ export class LockComponent implements OnInit, OnDestroy {
} }
// determine success route based on client type // determine success route based on client type
const successRoute = clientTypeToSuccessRouteRecord[this.clientType]; if (this.clientType != null) {
await this.router.navigate([successRoute]); const successRoute = clientTypeToSuccessRouteRecord[this.clientType];
await this.router.navigate([successRoute]);
}
} }
/** /**
@@ -608,7 +631,9 @@ export class LockComponent implements OnInit, OnDestroy {
private requirePasswordChange(): boolean { private requirePasswordChange(): boolean {
if ( if (
this.enforcedMasterPasswordOptions == undefined || this.enforcedMasterPasswordOptions == undefined ||
!this.enforcedMasterPasswordOptions.enforceOnLogin !this.enforcedMasterPasswordOptions.enforceOnLogin ||
this.formGroup == null ||
this.activeAccount == null
) { ) {
return false; return false;
} }
@@ -703,10 +728,13 @@ export class LockComponent implements OnInit, OnDestroy {
} }
get biometricsAvailable(): boolean { get biometricsAvailable(): boolean {
return this.unlockOptions.biometrics.enabled; return this.unlockOptions?.biometrics.enabled ?? false;
} }
get showBiometrics(): boolean { get showBiometrics(): boolean {
if (this.unlockOptions == null) {
return false;
}
return ( return (
this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.PlatformUnsupported && this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.PlatformUnsupported &&
this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.NotEnabledLocally this.unlockOptions.biometrics.biometricsStatus !== BiometricsStatus.NotEnabledLocally
@@ -714,7 +742,7 @@ export class LockComponent implements OnInit, OnDestroy {
} }
get biometricUnavailabilityReason(): string { get biometricUnavailabilityReason(): string {
switch (this.unlockOptions.biometrics.biometricsStatus) { switch (this.unlockOptions?.biometrics.biometricsStatus) {
case BiometricsStatus.Available: case BiometricsStatus.Available:
return ""; return "";
case BiometricsStatus.UnlockNeeded: case BiometricsStatus.UnlockNeeded:
@@ -728,19 +756,19 @@ export class LockComponent implements OnInit, OnDestroy {
case BiometricsStatus.NotEnabledInConnectedDesktopApp: case BiometricsStatus.NotEnabledInConnectedDesktopApp:
return this.i18nService.t( return this.i18nService.t(
"biometricsStatusHelptextNotEnabledInDesktop", "biometricsStatusHelptextNotEnabledInDesktop",
this.activeAccount.email, this.activeAccount?.email,
); );
case BiometricsStatus.NotEnabledLocally: case BiometricsStatus.NotEnabledLocally:
return this.i18nService.t( return this.i18nService.t(
"biometricsStatusHelptextNotEnabledInDesktop", "biometricsStatusHelptextNotEnabledInDesktop",
this.activeAccount.email, this.activeAccount?.email,
); );
case BiometricsStatus.DesktopDisconnected: case BiometricsStatus.DesktopDisconnected:
return this.i18nService.t("biometricsStatusHelptextDesktopDisconnected"); return this.i18nService.t("biometricsStatusHelptextDesktopDisconnected");
default: default:
return ( return (
this.i18nService.t("biometricsStatusHelptextUnavailableReasonUnknown") + this.i18nService.t("biometricsStatusHelptextUnavailableReasonUnknown") +
this.unlockOptions.biometrics.biometricsStatus this.unlockOptions?.biometrics.biometricsStatus
); );
} }
} }

View File

@@ -39,5 +39,5 @@ export abstract class LockComponentService {
abstract getBiometricsUnlockBtnText(): string; abstract getBiometricsUnlockBtnText(): string;
// Multi client // Multi client
abstract getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions>; abstract getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions | null>;
} }

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data";
@@ -40,7 +38,7 @@ export type CipherDecryptionKeys = {
/** /**
* A users decrypted organization keys. * A users decrypted organization keys.
*/ */
orgKeys: Record<OrganizationId, OrgKey>; orgKeys: Record<OrganizationId, OrgKey> | null;
}; };
export abstract class KeyService { export abstract class KeyService {
@@ -49,7 +47,7 @@ export abstract class KeyService {
* is in a locked or logged out state. * is in a locked or logged out state.
* @param userId The user id of the user to get the {@see UserKey} for. * @param userId The user id of the user to get the {@see UserKey} for.
*/ */
abstract userKey$(userId: UserId): Observable<UserKey>; abstract userKey$(userId: UserId): Observable<UserKey | null>;
/** /**
* Returns the an observable key for the given user id. * Returns the an observable key for the given user id.
* *
@@ -62,11 +60,11 @@ export abstract class KeyService {
* any other necessary versions (such as auto, biometrics, * any other necessary versions (such as auto, biometrics,
* or pin) * or pin)
* *
* @throws when key is null. Lock the account to clear a key * @throws Error when key or userId is null. Lock the account to clear a key.
* @param key The user key to set * @param key The user key to set
* @param userId The desired user * @param userId The desired user
*/ */
abstract setUserKey(key: UserKey, userId?: string): Promise<void>; abstract setUserKey(key: UserKey, userId: UserId): Promise<void>;
/** /**
* Sets the provided user keys and stores any other necessary versions * Sets the provided user keys and stores any other necessary versions
* (such as auto, biometrics, or pin). * (such as auto, biometrics, or pin).
@@ -129,7 +127,10 @@ export abstract class KeyService {
* @param userId The desired user * @param userId The desired user
* @returns The user key * @returns The user key
*/ */
abstract getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise<UserKey>; abstract getUserKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: string,
): Promise<UserKey | null>;
/** /**
* Determines whether the user key is available for the given user. * Determines whether the user key is available for the given user.
@@ -151,10 +152,11 @@ export abstract class KeyService {
abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean>; abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean>;
/** /**
* Generates a new user key * Generates a new user key
* @param masterKey The user's master key * @throws Error when master key is null and there is no active user
* @param masterKey The user's master key. When null, grabs master key from active user.
* @returns A new user key and the master key protected version of it * @returns A new user key and the master key protected version of it
*/ */
abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>; abstract makeUserKey(masterKey: MasterKey | null): Promise<[UserKey, EncString]>;
/** /**
* Clears the user's stored version of the user key * Clears the user's stored version of the user key
* @param keySuffix The desired version of the key to clear * @param keySuffix The desired version of the key to clear
@@ -163,11 +165,13 @@ export abstract class KeyService {
abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void>; abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
/** /**
* Stores the master key encrypted user key * Stores the master key encrypted user key
* @throws Error when userId is null and there is no active user.
* @param userKeyMasterKey The master key encrypted user key to set * @param userKeyMasterKey The master key encrypted user key to set
* @param userId The desired user * @param userId The desired user
*/ */
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId: string): Promise<void>; abstract setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void>;
/** /**
* @throws Error when userId is null and no active user
* @param password The user's master password that will be used to derive a master key if one isn't found * @param password The user's master password that will be used to derive a master key if one isn't found
* @param userId The desired user * @param userId The desired user
*/ */
@@ -195,14 +199,15 @@ export abstract class KeyService {
* Creates a master password hash from the user's master password. Can * Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending * be used for local authentication or for server authentication depending
* on the hashPurpose provided. * on the hashPurpose provided.
* @throws Error when password is null or key is null and no active user or active user have no master key
* @param password The user's master password * @param password The user's master password
* @param key The user's master key * @param key The user's master key or active's user master key.
* @param hashPurpose The iterations to use for the hash * @param hashPurpose The iterations to use for the hash
* @returns The user's master password hash * @returns The user's master password hash
*/ */
abstract hashMasterKey( abstract hashMasterKey(
password: string, password: string,
key: MasterKey, key: MasterKey | null,
hashPurpose?: HashPurpose, hashPurpose?: HashPurpose,
): Promise<string>; ): Promise<string>;
/** /**
@@ -240,13 +245,14 @@ export abstract class KeyService {
/** /**
* Returns the organization's symmetric key * Returns the organization's symmetric key
* @deprecated Use the observable userOrgKeys$ and `map` to the desired {@link OrgKey} instead * @deprecated Use the observable userOrgKeys$ and `map` to the desired {@link OrgKey} instead
* @throws Error when not active user
* @param orgId The desired organization * @param orgId The desired organization
* @returns The organization's symmetric key * @returns The organization's symmetric key
*/ */
abstract getOrgKey(orgId: string): Promise<OrgKey>; abstract getOrgKey(orgId: string): Promise<OrgKey | null>;
/** /**
* Uses the org key to derive a new symmetric key for encrypting data * Uses the org key to derive a new symmetric key for encrypting data
* @param orgKey The organization's symmetric key * @param key The organization's symmetric key
*/ */
abstract makeDataEncKey<T extends UserKey | OrgKey>( abstract makeDataEncKey<T extends UserKey | OrgKey>(
key: T, key: T,
@@ -259,13 +265,17 @@ export abstract class KeyService {
*/ */
abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>; abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>;
/** /**
*
* @throws Error when providerId is null or no active user
* @param providerId The desired provider * @param providerId The desired provider
* @returns The provider's symmetric key * @returns The provider's symmetric key
*/ */
abstract getProviderKey(providerId: string): Promise<ProviderKey>; abstract getProviderKey(providerId: string): Promise<ProviderKey | null>;
/** /**
* Creates a new organization key and encrypts it with the user's public key. * Creates a new organization key and encrypts it with the user's public key.
* This method can also return Provider keys for creating new Provider users. * This method can also return Provider keys for creating new Provider users.
*
* @throws Error when no active user or user have no public key
* @returns The new encrypted org key and the decrypted key itself * @returns The new encrypted org key and the decrypted key itself
*/ */
abstract makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]>; abstract makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]>;
@@ -281,11 +291,11 @@ export abstract class KeyService {
* from storage and stores it in memory * from storage and stores it in memory
* @returns The user's private key * @returns The user's private key
* *
* @throws An error if there is no user currently active. * @throws Error when no active user
* *
* @deprecated Use {@link userPrivateKey$} instead. * @deprecated Use {@link userPrivateKey$} instead.
*/ */
abstract getPrivateKey(): Promise<Uint8Array>; abstract getPrivateKey(): Promise<Uint8Array | null>;
/** /**
* Gets an observable stream of the given users decrypted private key, will emit null if the user * Gets an observable stream of the given users decrypted private key, will emit null if the user
@@ -294,7 +304,7 @@ export abstract class KeyService {
* *
* @param userId The user id of the user to get the data for. * @param userId The user id of the user to get the data for.
*/ */
abstract userPrivateKey$(userId: UserId): Observable<UserPrivateKey>; abstract userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null>;
/** /**
* Gets an observable stream of the given users encrypted private key, will emit null if the user * Gets an observable stream of the given users encrypted private key, will emit null if the user
@@ -305,7 +315,7 @@ export abstract class KeyService {
* @deprecated Temporary function to allow the SDK to be initialized after the login process, it * @deprecated Temporary function to allow the SDK to be initialized after the login process, it
* will be removed when auth has been migrated to the SDK. * will be removed when auth has been migrated to the SDK.
*/ */
abstract userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString>; abstract userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString | null>;
/** /**
* Gets an observable stream of the given users decrypted private key with legacy support, * Gets an observable stream of the given users decrypted private key with legacy support,
@@ -314,10 +324,12 @@ export abstract class KeyService {
* *
* @param userId The user id of the user to get the data for. * @param userId The user id of the user to get the data for.
*/ */
abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey>; abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey | null>;
/** /**
* Generates a fingerprint phrase for the user based on their public key * Generates a fingerprint phrase for the user based on their public key
*
* @throws Error when publicKey is null and there is no active user, or the active user does not have a public key
* @param fingerprintMaterial Fingerprint material * @param fingerprintMaterial Fingerprint material
* @param publicKey The user's public key * @param publicKey The user's public key
* @returns The user's fingerprint phrase * @returns The user's fingerprint phrase
@@ -410,7 +422,7 @@ export abstract class KeyService {
*/ */
abstract encryptedOrgKeys$( abstract encryptedOrgKeys$(
userId: UserId, userId: UserId,
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>>; ): Observable<Record<OrganizationId, EncryptedOrganizationKeyData> | null>;
/** /**
* Gets an observable stream of the users public key. If the user is does not have * Gets an observable stream of the users public key. If the user is does not have
@@ -420,7 +432,7 @@ export abstract class KeyService {
* *
* @throws If an invalid user id is passed in. * @throws If an invalid user id is passed in.
*/ */
abstract userPublicKey$(userId: UserId): Observable<UserPublicKey>; abstract userPublicKey$(userId: UserId): Observable<UserPublicKey | null>;
/** /**
* Validates that a userkey is correct for a given user * Validates that a userkey is correct for a given user

View File

@@ -117,40 +117,40 @@ describe("keyService", () => {
}); });
}); });
describe.each(["hasUserKey", "hasUserKeyInMemory"])( describe.each(["hasUserKey", "hasUserKeyInMemory"])(`%s`, (methodName: string) => {
`%s`, let mockUserKey: UserKey;
(method: "hasUserKey" | "hasUserKeyInMemory") => { let method: (userId?: UserId) => Promise<boolean>;
let mockUserKey: UserKey;
beforeEach(() => { beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray; const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
}); method =
methodName === "hasUserKey"
? keyService.hasUserKey.bind(keyService)
: keyService.hasUserKeyInMemory.bind(keyService);
});
it.each([true, false])("returns %s if the user key is set", async (hasKey) => { it.each([true, false])("returns %s if the user key is set", async (hasKey) => {
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(hasKey ? mockUserKey : null);
expect(await method(mockUserId)).toBe(hasKey);
});
it("returns false when no active userId is set", async () => {
accountService.activeAccountSubject.next(null);
expect(await method()).toBe(false);
});
it.each([true, false])(
"resolves %s for active user id when none is provided",
async (hasKey) => {
stateProvider.activeUserId$ = of(mockUserId);
stateProvider.singleUser stateProvider.singleUser
.getFake(mockUserId, USER_KEY) .getFake(mockUserId, USER_KEY)
.nextState(hasKey ? mockUserKey : null); .nextState(hasKey ? mockUserKey : null);
expect(await keyService[method](mockUserId)).toBe(hasKey); expect(await method()).toBe(hasKey);
}); },
);
it("returns false when no active userId is set", async () => { });
accountService.activeAccountSubject.next(null);
expect(await keyService[method]()).toBe(false);
});
it.each([true, false])(
"resolves %s for active user id when none is provided",
async (hasKey) => {
stateProvider.activeUserId$ = of(mockUserId);
stateProvider.singleUser
.getFake(mockUserId, USER_KEY)
.nextState(hasKey ? mockUserKey : null);
expect(await keyService[method]()).toBe(hasKey);
},
);
},
);
describe("getUserKeyWithLegacySupport", () => { describe("getUserKeyWithLegacySupport", () => {
let mockUserKey: UserKey; let mockUserKey: UserKey;
@@ -263,11 +263,15 @@ describe("keyService", () => {
}); });
it("throws if key is null", async () => { it("throws if key is null", async () => {
await expect(keyService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided."); await expect(keyService.setUserKey(null as unknown as UserKey, mockUserId)).rejects.toThrow(
"No key provided.",
);
}); });
it("throws if userId is null", async () => { it("throws if userId is null", async () => {
await expect(keyService.setUserKey(mockUserKey, null)).rejects.toThrow("No userId provided."); await expect(keyService.setUserKey(mockUserKey, null as unknown as UserId)).rejects.toThrow(
"No userId provided.",
);
}); });
describe("Pin Key refresh", () => { describe("Pin Key refresh", () => {
@@ -338,21 +342,21 @@ describe("keyService", () => {
}); });
it("throws if userKey is null", async () => { it("throws if userKey is null", async () => {
await expect(keyService.setUserKeys(null, mockEncPrivateKey, mockUserId)).rejects.toThrow( await expect(
"No userKey provided.", keyService.setUserKeys(null as unknown as UserKey, mockEncPrivateKey, mockUserId),
); ).rejects.toThrow("No userKey provided.");
}); });
it("throws if encPrivateKey is null", async () => { it("throws if encPrivateKey is null", async () => {
await expect(keyService.setUserKeys(mockUserKey, null, mockUserId)).rejects.toThrow( await expect(
"No encPrivateKey provided.", keyService.setUserKeys(mockUserKey, null as unknown as EncryptedString, mockUserId),
); ).rejects.toThrow("No encPrivateKey provided.");
}); });
it("throws if userId is null", async () => { it("throws if userId is null", async () => {
await expect(keyService.setUserKeys(mockUserKey, mockEncPrivateKey, null)).rejects.toThrow( await expect(
"No userId provided.", keyService.setUserKeys(mockUserKey, mockEncPrivateKey, null as unknown as UserId),
); ).rejects.toThrow("No userId provided.");
}); });
it("throws if encPrivateKey cannot be decrypted with the userKey", async () => { it("throws if encPrivateKey cannot be decrypted with the userKey", async () => {
@@ -388,7 +392,7 @@ describe("keyService", () => {
let callCount = 0; let callCount = 0;
stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++)); stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++));
await keyService.clearKeys(null); await keyService.clearKeys();
expect(callCount).toBe(1); expect(callCount).toBe(1);
// revert to the original state // revert to the original state
@@ -402,7 +406,7 @@ describe("keyService", () => {
USER_KEY, USER_KEY,
])("key removal", (key: UserKeyDefinition<unknown>) => { ])("key removal", (key: UserKeyDefinition<unknown>) => {
it(`clears ${key.key} for active user when unspecified`, async () => { it(`clears ${key.key} for active user when unspecified`, async () => {
await keyService.clearKeys(null); await keyService.clearKeys();
const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key); const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
@@ -426,7 +430,10 @@ describe("keyService", () => {
makeUserKey: boolean; makeUserKey: boolean;
}; };
function setupKeys({ makeMasterKey, makeUserKey }: SetupKeysParams): [UserKey, MasterKey] { function setupKeys({
makeMasterKey,
makeUserKey,
}: SetupKeysParams): [UserKey | null, MasterKey | null] {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY); const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null; const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null;
masterPasswordService.masterKeySubject.next(fakeMasterKey); masterPasswordService.masterKeySubject.next(fakeMasterKey);
@@ -449,7 +456,7 @@ describe("keyService", () => {
const fakeEncryptedUserPrivateKey = makeEncString("1"); const fakeEncryptedUserPrivateKey = makeEncString("1");
userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString); userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString!);
// Decryption of the user private key // Decryption of the user private key
const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1); const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1);
@@ -526,7 +533,7 @@ describe("keyService", () => {
function updateKeys(keys: Partial<UpdateKeysParams> = {}) { function updateKeys(keys: Partial<UpdateKeysParams> = {}) {
if ("userKey" in keys) { if ("userKey" in keys) {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY); const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
userKeyState.nextState(keys.userKey); userKeyState.nextState(keys.userKey!);
} }
if ("encryptedPrivateKey" in keys) { if ("encryptedPrivateKey" in keys) {
@@ -534,7 +541,7 @@ describe("keyService", () => {
mockUserId, mockUserId,
USER_ENCRYPTED_PRIVATE_KEY, USER_ENCRYPTED_PRIVATE_KEY,
); );
userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey.encryptedString); userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey!.encryptedString!);
} }
if ("orgKeys" in keys) { if ("orgKeys" in keys) {
@@ -542,7 +549,7 @@ describe("keyService", () => {
mockUserId, mockUserId,
USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ENCRYPTED_ORGANIZATION_KEYS,
); );
orgKeysState.nextState(keys.orgKeys); orgKeysState.nextState(keys.orgKeys!);
} }
if ("providerKeys" in keys) { if ("providerKeys" in keys) {
@@ -550,7 +557,7 @@ describe("keyService", () => {
mockUserId, mockUserId,
USER_ENCRYPTED_PROVIDER_KEYS, USER_ENCRYPTED_PROVIDER_KEYS,
); );
providerKeysState.nextState(keys.providerKeys); providerKeysState.nextState(keys.providerKeys!);
} }
encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => { encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => {
@@ -572,8 +579,8 @@ describe("keyService", () => {
const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId)); const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull(); expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull(); expect(decryptionKeys!.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).toEqual({}); expect(decryptionKeys!.orgKeys).toEqual({});
}); });
it("returns decryption keys when there are org keys", async () => { it("returns decryption keys when there are org keys", async () => {
@@ -581,18 +588,18 @@ describe("keyService", () => {
userKey: makeSymmetricCryptoKey<UserKey>(64), userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"), encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: { orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString }, [org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString! },
}, },
}); });
const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId)); const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull(); expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull(); expect(decryptionKeys!.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull(); expect(decryptionKeys!.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(1); expect(Object.keys(decryptionKeys!.orgKeys!)).toHaveLength(1);
expect(decryptionKeys.orgKeys[org1Id]).not.toBeNull(); expect(decryptionKeys!.orgKeys![org1Id]).not.toBeNull();
const orgKey = decryptionKeys.orgKeys[org1Id]; const orgKey = decryptionKeys!.orgKeys![org1Id];
expect(orgKey.keyB64).toContain("org1Key"); expect(orgKey.keyB64).toContain("org1Key");
}); });
@@ -601,7 +608,7 @@ describe("keyService", () => {
userKey: makeSymmetricCryptoKey<UserKey>(64), userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"), encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: { orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString }, [org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString! },
}, },
providerKeys: {}, providerKeys: {},
}); });
@@ -609,11 +616,11 @@ describe("keyService", () => {
const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId)); const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull(); expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull(); expect(decryptionKeys!.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull(); expect(decryptionKeys!.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(1); expect(Object.keys(decryptionKeys!.orgKeys!)).toHaveLength(1);
expect(decryptionKeys.orgKeys[org1Id]).not.toBeNull(); expect(decryptionKeys!.orgKeys![org1Id]).not.toBeNull();
const orgKey = decryptionKeys.orgKeys[org1Id]; const orgKey = decryptionKeys!.orgKeys![org1Id];
expect(orgKey.keyB64).toContain("org1Key"); expect(orgKey.keyB64).toContain("org1Key");
}); });
@@ -623,30 +630,30 @@ describe("keyService", () => {
userKey: makeSymmetricCryptoKey<UserKey>(64), userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"), encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: { orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString }, [org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString! },
[org2Id]: { [org2Id]: {
type: "provider", type: "provider",
key: makeEncString("provider1Key").encryptedString, key: makeEncString("provider1Key").encryptedString!,
providerId: "provider1", providerId: "provider1",
}, },
}, },
providerKeys: { providerKeys: {
provider1: makeEncString("provider1Key").encryptedString, provider1: makeEncString("provider1Key").encryptedString!,
}, },
}); });
const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId)); const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull(); expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull(); expect(decryptionKeys!.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull(); expect(decryptionKeys!.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(2); expect(Object.keys(decryptionKeys!.orgKeys!)).toHaveLength(2);
const orgKey = decryptionKeys.orgKeys[org1Id]; const orgKey = decryptionKeys!.orgKeys![org1Id];
expect(orgKey).not.toBeNull(); expect(orgKey).not.toBeNull();
expect(orgKey.keyB64).toContain("org1Key"); expect(orgKey.keyB64).toContain("org1Key");
const org2Key = decryptionKeys.orgKeys[org2Id]; const org2Key = decryptionKeys!.orgKeys![org2Id];
expect(org2Key).not.toBeNull(); expect(org2Key).not.toBeNull();
expect(org2Key.keyB64).toContain("provider1Key"); expect(org2Key.keyB64).toContain("provider1Key");
}); });
@@ -686,7 +693,7 @@ describe("keyService", () => {
// User has their org keys set // User has their org keys set
updateKeys({ updateKeys({
orgKeys: { orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString }, [org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString! },
}, },
}); });
@@ -741,7 +748,7 @@ describe("keyService", () => {
type TestCase = { type TestCase = {
masterKey: MasterKey; masterKey: MasterKey;
masterPassword: string | null; masterPassword: string | null;
storedMasterKeyHash: string; storedMasterKeyHash: string | null;
mockReturnedHash: string; mockReturnedHash: string;
expectedToMatch: boolean; expectedToMatch: boolean;
}; };
@@ -782,7 +789,7 @@ describe("keyService", () => {
masterPasswordService.masterKeyHashSubject.next(storedMasterKeyHash); masterPasswordService.masterKeyHashSubject.next(storedMasterKeyHash);
cryptoFunctionService.pbkdf2 cryptoFunctionService.pbkdf2
.calledWith(masterKey.key, masterPassword, "sha256", 2) .calledWith(masterKey.key, masterPassword as string, "sha256", 2)
.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHash)); .mockResolvedValue(Utils.fromB64ToArray(mockReturnedHash));
const actualDidMatch = await keyService.compareKeyHash( const actualDidMatch = await keyService.compareKeyHash(

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as bigInt from "big-integer"; import * as bigInt from "big-integer";
import { import {
NEVER, NEVER,
@@ -88,7 +86,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe( this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe(
switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)), switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)),
); ) as Observable<Record<OrganizationId, OrgKey>>;
} }
async setUserKey(key: UserKey, userId: UserId): Promise<void> { async setUserKey(key: UserKey, userId: UserId): Promise<void> {
@@ -152,14 +150,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> { async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$); userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user id found.");
}
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
return await this.validateUserKey(masterKey as unknown as UserKey, userId); return await this.validateUserKey(masterKey, userId);
} }
// TODO: legacy support for user key is no longer needed since we require users to migrate on login // TODO: legacy support for user key is no longer needed since we require users to migrate on login
async getUserKeyWithLegacySupport(userId?: UserId): Promise<UserKey> { async getUserKeyWithLegacySupport(userId?: UserId): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$); userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user id found.");
}
const userKey = await this.getUserKey(userId); const userKey = await this.getUserKey(userId);
if (userKey) { if (userKey) {
@@ -172,16 +176,25 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return masterKey as unknown as UserKey; return masterKey as unknown as UserKey;
} }
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> { async getUserKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: UserId,
): Promise<UserKey | null> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$); userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
const userKey = await this.getKeyFromStorage(keySuffix, userId); if (userId == null) {
if (userKey) { throw new Error("No active user id found.");
if (!(await this.validateUserKey(userKey, userId))) {
this.logService.warning("Invalid key, throwing away stored keys");
await this.clearAllStoredUserKeys(userId);
}
return userKey;
} }
const userKey = await this.getKeyFromStorage(keySuffix, userId);
if (userKey == null) {
return null;
}
if (!(await this.validateUserKey(userKey, userId))) {
this.logService.warning("Invalid key, throwing away stored keys");
await this.clearAllStoredUserKeys(userId);
}
return userKey;
} }
async hasUserKey(userId?: UserId): Promise<boolean> { async hasUserKey(userId?: UserId): Promise<boolean> {
@@ -205,9 +218,13 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return (await this.getKeyFromStorage(keySuffix, userId)) != null; return (await this.getKeyFromStorage(keySuffix, userId)) != null;
} }
async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> { async makeUserKey(masterKey: MasterKey | null): Promise<[UserKey, EncString]> {
if (!masterKey) { if (masterKey == null) {
const userId = await firstValueFrom(this.stateProvider.activeUserId$); const userId = await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user id found.");
}
masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
} }
if (masterKey == null) { if (masterKey == null) {
@@ -241,7 +258,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId); this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
} }
if (keySuffix === KeySuffixOptions.Pin) { if (keySuffix === KeySuffixOptions.Pin && userId != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId); this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
@@ -251,8 +268,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
} }
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise<void> { async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$); userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user id found.");
}
await this.masterPasswordService.setMasterKeyEncryptedUserKey( await this.masterPasswordService.setMasterKeyEncryptedUserKey(
new EncString(userKeyMasterKey), new EncString(userKeyMasterKey),
userId, userId,
@@ -265,16 +286,18 @@ export class DefaultKeyService implements KeyServiceAbstraction {
combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe( combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe(
map(([activeAccount, accounts]) => { map(([activeAccount, accounts]) => {
userId ??= activeAccount?.id; userId ??= activeAccount?.id;
return [userId, accounts[userId]?.email]; if (userId == null || accounts[userId] == null) {
throw new Error("No user found");
}
return [userId, accounts[userId].email];
}), }),
), ),
); );
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId)); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
return (masterKey ||= await this.makeMasterKey( return (
password, masterKey ||
email, (await this.makeMasterKey(password, email, await this.kdfConfigService.getKdfConfig()))
await this.kdfConfigService.getKdfConfig(), );
));
} }
/** /**
@@ -303,11 +326,15 @@ export class DefaultKeyService implements KeyServiceAbstraction {
// TODO: move to MasterPasswordService // TODO: move to MasterPasswordService
async hashMasterKey( async hashMasterKey(
password: string, password: string,
key: MasterKey, key: MasterKey | null,
hashPurpose?: HashPurpose, hashPurpose?: HashPurpose,
): Promise<string> { ): Promise<string> {
if (!key) { if (key == null) {
const userId = await firstValueFrom(this.stateProvider.activeUserId$); const userId = await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user found.");
}
key = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); key = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
} }
@@ -322,7 +349,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
// TODO: move to MasterPasswordService // TODO: move to MasterPasswordService
async compareKeyHash( async compareKeyHash(
masterPassword: string, masterPassword: string | null,
masterKey: MasterKey, masterKey: MasterKey,
userId: UserId, userId: UserId,
): Promise<boolean> { ): Promise<boolean> {
@@ -386,13 +413,13 @@ export class DefaultKeyService implements KeyServiceAbstraction {
}); });
} }
async getOrgKey(orgId: OrganizationId): Promise<OrgKey> { async getOrgKey(orgId: OrganizationId): Promise<OrgKey | null> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) { if (activeUserId == null) {
throw new Error("A user must be active to retrieve an org key"); throw new Error("A user must be active to retrieve an org key");
} }
const orgKeys = await firstValueFrom(this.orgKeys$(activeUserId)); const orgKeys = await firstValueFrom(this.orgKeys$(activeUserId));
return orgKeys[orgId]; return orgKeys?.[orgId] ?? null;
} }
async makeDataEncKey<T extends OrgKey | UserKey>( async makeDataEncKey<T extends OrgKey | UserKey>(
@@ -427,15 +454,19 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
// TODO: Deprecate in favor of observable // TODO: Deprecate in favor of observable
async getProviderKey(providerId: ProviderId): Promise<ProviderKey> { async getProviderKey(providerId: ProviderId): Promise<ProviderKey | null> {
if (providerId == null) { if (providerId == null) {
return null; return null;
} }
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
throw new Error("No active user found.");
}
const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId)); const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId));
return providerKeys[providerId] ?? null; return providerKeys?.[providerId] ?? null;
} }
private async clearProviderKeys(userId: UserId): Promise<void> { private async clearProviderKeys(userId: UserId): Promise<void> {
@@ -450,7 +481,15 @@ export class DefaultKeyService implements KeyServiceAbstraction {
async makeOrgKey<T extends OrgKey | ProviderKey>(userId?: UserId): Promise<[EncString, T]> { async makeOrgKey<T extends OrgKey | ProviderKey>(userId?: UserId): Promise<[EncString, T]> {
const shareKey = await this.keyGenerationService.createKey(512); const shareKey = await this.keyGenerationService.createKey(512);
userId ??= await firstValueFrom(this.stateProvider.activeUserId$); userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("No active user found.");
}
const publicKey = await firstValueFrom(this.userPublicKey$(userId)); const publicKey = await firstValueFrom(this.userPublicKey$(userId));
if (publicKey == null) {
throw new Error("No public key found.");
}
const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey); const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey);
return [encShareKey, shareKey as T]; return [encShareKey, shareKey as T];
} }
@@ -465,7 +504,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
.update(() => encPrivateKey); .update(() => encPrivateKey);
} }
async getPrivateKey(): Promise<Uint8Array> { async getPrivateKey(): Promise<Uint8Array | null> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) { if (activeUserId == null) {
@@ -479,7 +518,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> { async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
if (publicKey == null) { if (publicKey == null) {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
publicKey = await firstValueFrom(this.userPublicKey$(activeUserId)); if (activeUserId == null) {
throw new Error("No active user found.");
}
publicKey = (await firstValueFrom(this.userPublicKey$(activeUserId))) as Uint8Array;
} }
if (publicKey === null) { if (publicKey === null) {
@@ -510,12 +552,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
* Clears the user's key pair * Clears the user's key pair
* @param userId The desired user * @param userId The desired user
*/ */
private async clearKeyPair(userId: UserId): Promise<void[]> { private async clearKeyPair(userId: UserId): Promise<void> {
if (userId == null) {
// nothing to do
return;
}
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
} }
@@ -596,8 +633,8 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
// ---HELPERS--- // ---HELPERS---
async validateUserKey(key: UserKey, userId: UserId): Promise<boolean> { async validateUserKey(key: UserKey | MasterKey | null, userId: UserId): Promise<boolean> {
if (!key) { if (key == null) {
return false; return false;
} }
@@ -659,10 +696,14 @@ export class DefaultKeyService implements KeyServiceAbstraction {
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey; const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
const [publicKey, privateKey] = await this.makeKeyPair(userKey); const [publicKey, privateKey] = await this.makeKeyPair(userKey);
if (privateKey.encryptedString == null) {
throw new Error("Failed to create valid private key.");
}
await this.setUserKey(userKey, activeUserId); await this.setUserKey(userKey, activeUserId);
await this.stateProvider await this.stateProvider
.getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY) .getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => privateKey.encryptedString); .update(() => privateKey.encryptedString!);
return { return {
userKey, userKey,
@@ -692,7 +733,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
if (storePin) { if (storePin) {
// Decrypt userKeyEncryptedPin with user key // Decrypt userKeyEncryptedPin with user key
const pin = await this.encryptService.decryptToUtf8( const pin = await this.encryptService.decryptToUtf8(
await this.pinService.getUserKeyEncryptedPin(userId), (await this.pinService.getUserKeyEncryptedPin(userId))!,
key, key,
); );
@@ -718,7 +759,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
} }
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId) { protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId) {
let shouldStoreKey = false; let shouldStoreKey = false;
switch (keySuffix) { switch (keySuffix) {
case KeySuffixOptions.Auto: { case KeySuffixOptions.Auto: {
@@ -744,7 +785,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
protected async getKeyFromStorage( protected async getKeyFromStorage(
keySuffix: KeySuffixOptions, keySuffix: KeySuffixOptions,
userId?: UserId, userId?: UserId,
): Promise<UserKey> { ): Promise<UserKey | null> {
if (keySuffix === KeySuffixOptions.Auto) { if (keySuffix === KeySuffixOptions.Auto) {
const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId }); const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId });
if (userKey) { if (userKey) {
@@ -754,7 +795,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return null; return null;
} }
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> { protected async clearAllStoredUserKeys(userId: UserId): Promise<void> {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId); await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
} }
@@ -783,7 +824,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
encryptionKey: SymmetricCryptoKey, encryptionKey: SymmetricCryptoKey,
newSymKey: Uint8Array, newSymKey: Uint8Array,
): Promise<[T, EncString]> { ): Promise<[T, EncString]> {
let protectedSymKey: EncString = null; let protectedSymKey: EncString;
if (encryptionKey.key.byteLength === 32) { if (encryptionKey.key.byteLength === 32) {
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey); const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey); protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
@@ -803,12 +844,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) { async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) {
if (keySuffix === KeySuffixOptions.Auto) { if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
} else if (keySuffix === KeySuffixOptions.Pin) { } else if (keySuffix === KeySuffixOptions.Pin && userId != null) {
await this.pinService.clearOldPinKeyEncryptedMasterKey(userId); await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
} }
} }
userKey$(userId: UserId): Observable<UserKey> { userKey$(userId: UserId): Observable<UserKey | null> {
return this.stateProvider.getUser(userId, USER_KEY).state$; return this.stateProvider.getUser(userId, USER_KEY).state$;
} }
@@ -822,7 +863,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
// Legacy path // Legacy path
return this.masterPasswordService.masterKey$(userId).pipe( return this.masterPasswordService.masterKey$(userId).pipe(
switchMap(async (masterKey) => { switchMap(async (masterKey) => {
if (!(await this.validateUserKey(masterKey as unknown as UserKey, userId))) { if (!(await this.validateUserKey(masterKey, userId))) {
// We don't have a UserKey or a valid MasterKey // We don't have a UserKey or a valid MasterKey
return null; return null;
} }
@@ -841,7 +882,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
); );
} }
private async derivePublicKey(privateKey: UserPrivateKey) { private async derivePublicKey(privateKey: UserPrivateKey | null) {
if (privateKey == null) { if (privateKey == null) {
return null; return null;
} }
@@ -849,16 +890,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
} }
userPrivateKey$(userId: UserId): Observable<UserPrivateKey> { userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null> {
return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey)); return this.userPrivateKeyHelper$(userId, false).pipe(
map((keys) => keys?.userPrivateKey ?? null),
);
} }
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString> { userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString | null> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$; return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
} }
userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey> { userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey | null> {
return this.userPrivateKeyHelper$(userId, true).pipe(map((keys) => keys?.userPrivateKey)); return this.userPrivateKeyHelper$(userId, true).pipe(
map((keys) => keys?.userPrivateKey ?? null),
);
} }
private userPrivateKeyHelper$(userId: UserId, legacySupport: boolean) { private userPrivateKeyHelper$(userId: UserId, legacySupport: boolean) {
@@ -884,7 +929,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
); );
} }
private async decryptPrivateKey(encryptedPrivateKey: EncryptedString, key: SymmetricCryptoKey) { private async decryptPrivateKey(
encryptedPrivateKey: EncryptedString | null,
key: SymmetricCryptoKey,
) {
if (encryptedPrivateKey == null) { if (encryptedPrivateKey == null) {
return null; return null;
} }
@@ -916,7 +964,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
private providerKeysHelper$( private providerKeysHelper$(
userId: UserId, userId: UserId,
userPrivateKey: UserPrivateKey, userPrivateKey: UserPrivateKey,
): Observable<Record<ProviderId, ProviderKey>> { ): Observable<Record<ProviderId, ProviderKey> | null> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).state$.pipe( return this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).state$.pipe(
// Convert each value in the record to it's own decryption observable // Convert each value in the record to it's own decryption observable
convertValues(async (_, value) => { convertValues(async (_, value) => {
@@ -943,12 +991,12 @@ export class DefaultKeyService implements KeyServiceAbstraction {
} }
orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null> { orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null> {
return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys)); return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys ?? null));
} }
encryptedOrgKeys$( encryptedOrgKeys$(
userId: UserId, userId: UserId,
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>> { ): Observable<Record<OrganizationId, EncryptedOrganizationKeyData> | null> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$; return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$;
} }
@@ -956,7 +1004,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
userId: UserId, userId: UserId,
legacySupport: boolean = false, legacySupport: boolean = false,
): Observable<CipherDecryptionKeys | null> { ): Observable<CipherDecryptionKeys | null> {
return this.userPrivateKeyHelper$(userId, legacySupport).pipe( return this.userPrivateKeyHelper$(userId, legacySupport)?.pipe(
switchMap((userKeys) => { switchMap((userKeys) => {
if (userKeys == null) { if (userKeys == null) {
return of(null); return of(null);
@@ -975,15 +1023,22 @@ export class DefaultKeyService implements KeyServiceAbstraction {
]).pipe( ]).pipe(
switchMap(async ([encryptedOrgKeys, providerKeys]) => { switchMap(async ([encryptedOrgKeys, providerKeys]) => {
const result: Record<OrganizationId, OrgKey> = {}; const result: Record<OrganizationId, OrgKey> = {};
for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) { encryptedOrgKeys = encryptedOrgKeys ?? {};
for (const orgId of Object.keys(encryptedOrgKeys) as OrganizationId[]) {
if (result[orgId] != null) { if (result[orgId] != null) {
continue; continue;
} }
const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]); const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]);
if (encrypted == null) {
continue;
}
let decrypted: OrgKey; let decrypted: OrgKey;
if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) { if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) {
if (providerKeys == null) {
throw new Error("No provider keys found.");
}
decrypted = await encrypted.decrypt(this.encryptService, providerKeys); decrypted = await encrypted.decrypt(this.encryptService, providerKeys);
} else { } else {
decrypted = await encrypted.decrypt(this.encryptService, userPrivateKey); decrypted = await encrypted.decrypt(this.encryptService, userPrivateKey);

View File

@@ -119,6 +119,9 @@ export class DefaultUserAsymmetricKeysRegenerationService
private async regenerateUserAsymmetricKeys(userId: UserId): Promise<void> { private async regenerateUserAsymmetricKeys(userId: UserId): Promise<void> {
const userKey = await firstValueFrom(this.keyService.userKey$(userId)); const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (userKey == null) {
throw new Error("User key not found");
}
const makeKeyPairResponse = await firstValueFrom( const makeKeyPairResponse = await firstValueFrom(
this.sdkService.client$.pipe( this.sdkService.client$.pipe(
map((sdk) => { map((sdk) => {