mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +00:00
[PM-24128] New Pin service, using PasswordProtectedKeyEnvelope (#15863)
* fix: broken SDK interface * Fix all compile errors related to uuids * Update usages of sdk to type-safe SDK type * Update sdk version * Update to "toSdk" * Move pin service to km ownership * Run format * Eslint * Fix tsconfig * Fix imports and test * Clean up imports * Pin tmp * Initial version of updated pin service * Add tests * Rename function * Clean up logging * Fix imports * Fix cli build * Fix browser desktop * Fix tests * Attempt to fix * Fix build * Fix tests * Fix browser build * Add missing empty line * Fix linting * Remove non-required change * Missing newline * Re-add comment * Undo change to file * Fix missing empty line * Cleanup * Cleanup * Cleanup * Cleanup * Switch to replaysubject * Add comments * Fix tests * Run prettier * Undo change * Fix browser * Fix circular dependency on browser * Add missing clear ephemeral pin * Address feedback * Update docs * Simplify sdk usage in pin service * Replace with mock sdk * Update sdk * Initialize pin service via unlock instead of listening to keyservice * Cleanup * Fix test * Prevent race condition with userkey not being set * Filter null userkeys * [PM-24124] Pin State Service (#16641) * add pin-state.service * add remaining tests * improve description for clearEphemeralPinState * rename getUserKeyWrappedPin$ to userKeyWrappedPin$ * drop temp variable in setPinState * add new test and remove copied one * Fix dep cycle * Fix tests and remaining build issues * Fix cli build * Add comments about functions not being public API --------- Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com> Co-authored-by: Hinton <hinton@users.noreply.github.com> Co-authored-by: Jake Fink <jfink@bitwarden.com>
This commit is contained in:
@@ -1,275 +1,137 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig, KdfConfigService } from "@bitwarden/key-management";
|
||||
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service";
|
||||
import { assertNonNullish } from "../../auth/utils";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString, EncryptedString } from "../../key-management/crypto/models/enc-string";
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { PIN_DISK, PIN_MEMORY, StateProvider, UserKeyDefinition } from "../../platform/state";
|
||||
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { PinKey, UserKey } from "../../types/key";
|
||||
import { KeyGenerationService } from "../crypto";
|
||||
import { firstValueFromOrThrow } from "../utils";
|
||||
|
||||
import { PinLockType } from "./pin-lock-type";
|
||||
import { PinStateServiceAbstraction } from "./pin-state.service.abstraction";
|
||||
import { PinServiceAbstraction } from "./pin.service.abstraction";
|
||||
|
||||
/**
|
||||
* - DISABLED : No PIN set.
|
||||
* - PERSISTENT : PIN is set and persists through client reset.
|
||||
* - EPHEMERAL : PIN is set, but does NOT persist through client reset. This means that
|
||||
* after client reset the master password is required to unlock.
|
||||
*/
|
||||
export type PinLockType = "DISABLED" | "PERSISTENT" | "EPHEMERAL";
|
||||
|
||||
/**
|
||||
* The persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
|
||||
*
|
||||
* @remarks Persists through a client reset. Used when `requireMasterPasswordOnClientRestart` is disabled.
|
||||
* @see SetPinComponent.setPinForm.requireMasterPasswordOnClientRestart
|
||||
*/
|
||||
export const PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT = new UserKeyDefinition<EncryptedString>(
|
||||
PIN_DISK,
|
||||
"pinKeyEncryptedUserKeyPersistent",
|
||||
{
|
||||
deserializer: (jsonValue) => jsonValue,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
|
||||
*
|
||||
* @remarks Does NOT persist through a client reset. Used when `requireMasterPasswordOnClientRestart` is enabled.
|
||||
* @see SetPinComponent.setPinForm.requireMasterPasswordOnClientRestart
|
||||
*/
|
||||
export const PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL = new UserKeyDefinition<EncryptedString>(
|
||||
PIN_MEMORY,
|
||||
"pinKeyEncryptedUserKeyEphemeral",
|
||||
{
|
||||
deserializer: (jsonValue) => jsonValue,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The PIN, encrypted by the UserKey.
|
||||
*/
|
||||
export const USER_KEY_ENCRYPTED_PIN = new UserKeyDefinition<EncryptedString>(
|
||||
PIN_DISK,
|
||||
"userKeyEncryptedPin",
|
||||
{
|
||||
deserializer: (jsonValue) => jsonValue,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export class PinService implements PinServiceAbstraction {
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private encryptService: EncryptService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private logService: LogService,
|
||||
private stateProvider: StateProvider,
|
||||
private keyService: KeyService,
|
||||
private sdkService: SdkService,
|
||||
private pinStateService: PinStateServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async getPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString | null> {
|
||||
this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyPersistent.");
|
||||
|
||||
return EncString.fromJSON(
|
||||
await firstValueFrom(
|
||||
this.stateProvider.getUserState$(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, userId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
|
||||
*/
|
||||
private async setPinKeyEncryptedUserKeyPersistent(
|
||||
pinKeyEncryptedUserKey: EncString,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
this.validateUserId(userId, "Cannot set pinKeyEncryptedUserKeyPersistent.");
|
||||
|
||||
if (pinKeyEncryptedUserKey == null) {
|
||||
throw new Error(
|
||||
"No pinKeyEncryptedUserKey provided. Cannot set pinKeyEncryptedUserKeyPersistent.",
|
||||
);
|
||||
}
|
||||
|
||||
await this.stateProvider.setUserState(
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
pinKeyEncryptedUserKey?.encryptedString,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
async clearPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<void> {
|
||||
this.validateUserId(userId, "Cannot clear pinKeyEncryptedUserKeyPersistent.");
|
||||
|
||||
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, userId);
|
||||
}
|
||||
|
||||
async getPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<EncString | null> {
|
||||
this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyEphemeral.");
|
||||
|
||||
return EncString.fromJSON(
|
||||
await firstValueFrom(
|
||||
this.stateProvider.getUserState$(PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL, userId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
|
||||
*/
|
||||
private async setPinKeyEncryptedUserKeyEphemeral(
|
||||
pinKeyEncryptedUserKey: EncString,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
this.validateUserId(userId, "Cannot set pinKeyEncryptedUserKeyEphemeral.");
|
||||
|
||||
if (pinKeyEncryptedUserKey == null) {
|
||||
throw new Error(
|
||||
"No pinKeyEncryptedUserKey provided. Cannot set pinKeyEncryptedUserKeyEphemeral.",
|
||||
);
|
||||
}
|
||||
|
||||
await this.stateProvider.setUserState(
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
|
||||
pinKeyEncryptedUserKey?.encryptedString,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
async clearPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<void> {
|
||||
this.validateUserId(userId, "Cannot clear pinKeyEncryptedUserKeyEphemeral.");
|
||||
|
||||
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL, null, userId);
|
||||
}
|
||||
|
||||
async createPinKeyEncryptedUserKey(
|
||||
pin: string,
|
||||
userKey: UserKey,
|
||||
userId: UserId,
|
||||
): Promise<EncString> {
|
||||
this.validateUserId(userId, "Cannot create pinKeyEncryptedUserKey.");
|
||||
|
||||
if (!userKey) {
|
||||
throw new Error("No UserKey provided. Cannot create pinKeyEncryptedUserKey.");
|
||||
}
|
||||
|
||||
const email = await firstValueFrom(
|
||||
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
|
||||
);
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
const pinKey = await this.makePinKey(pin, email, kdfConfig);
|
||||
|
||||
return await this.encryptService.wrapSymmetricKey(userKey, pinKey);
|
||||
}
|
||||
|
||||
async storePinKeyEncryptedUserKey(
|
||||
pinKeyEncryptedUserKey: EncString,
|
||||
storeAsEphemeral: boolean,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
this.validateUserId(userId, "Cannot store pinKeyEncryptedUserKey.");
|
||||
|
||||
if (storeAsEphemeral) {
|
||||
await this.setPinKeyEncryptedUserKeyEphemeral(pinKeyEncryptedUserKey, userId);
|
||||
} else {
|
||||
await this.setPinKeyEncryptedUserKeyPersistent(pinKeyEncryptedUserKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
async getUserKeyEncryptedPin(userId: UserId): Promise<EncString | null> {
|
||||
this.validateUserId(userId, "Cannot get userKeyEncryptedPin.");
|
||||
|
||||
return EncString.fromJSON(
|
||||
await firstValueFrom(this.stateProvider.getUserState$(USER_KEY_ENCRYPTED_PIN, userId)),
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKeyEncryptedPin(userKeyEncryptedPin: EncString, userId: UserId): Promise<void> {
|
||||
this.validateUserId(userId, "Cannot set userKeyEncryptedPin.");
|
||||
|
||||
await this.stateProvider.setUserState(
|
||||
USER_KEY_ENCRYPTED_PIN,
|
||||
userKeyEncryptedPin?.encryptedString,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
async clearUserKeyEncryptedPin(userId: UserId): Promise<void> {
|
||||
this.validateUserId(userId, "Cannot clear userKeyEncryptedPin.");
|
||||
|
||||
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, null, userId);
|
||||
}
|
||||
|
||||
async createUserKeyEncryptedPin(pin: string, userKey: UserKey): Promise<EncString> {
|
||||
if (!userKey) {
|
||||
throw new Error("No UserKey provided. Cannot create userKeyEncryptedPin.");
|
||||
}
|
||||
|
||||
return await this.encryptService.encryptString(pin, userKey);
|
||||
}
|
||||
|
||||
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
|
||||
const start = Date.now();
|
||||
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
|
||||
this.logService.info(`[Pin Service] deriving pin key took ${Date.now() - start}ms`);
|
||||
|
||||
return (await this.keyGenerationService.stretchKey(pinKey)) as PinKey;
|
||||
}
|
||||
|
||||
async getPinLockType(userId: UserId): Promise<PinLockType> {
|
||||
this.validateUserId(userId, "Cannot get PinLockType.");
|
||||
|
||||
const aUserKeyEncryptedPinIsSet = !!(await this.getUserKeyEncryptedPin(userId));
|
||||
const aPinKeyEncryptedUserKeyPersistentIsSet =
|
||||
!!(await this.getPinKeyEncryptedUserKeyPersistent(userId));
|
||||
|
||||
if (aPinKeyEncryptedUserKeyPersistentIsSet) {
|
||||
return "PERSISTENT";
|
||||
} else if (aUserKeyEncryptedPinIsSet && !aPinKeyEncryptedUserKeyPersistentIsSet) {
|
||||
return "EPHEMERAL";
|
||||
} else {
|
||||
return "DISABLED";
|
||||
}
|
||||
getPinLockType(userId: UserId): Promise<PinLockType> {
|
||||
assertNonNullish(userId, "userId");
|
||||
return this.pinStateService.getPinLockType(userId);
|
||||
}
|
||||
|
||||
async isPinSet(userId: UserId): Promise<boolean> {
|
||||
this.validateUserId(userId, "Cannot determine if PIN is set.");
|
||||
assertNonNullish(userId, "userId");
|
||||
return (await this.pinStateService.getPinLockType(userId)) !== "DISABLED";
|
||||
}
|
||||
|
||||
return (await this.getPinLockType(userId)) !== "DISABLED";
|
||||
async logout(userId: UserId): Promise<void> {
|
||||
assertNonNullish(userId, "userId");
|
||||
await this.pinStateService.clearPinState(userId);
|
||||
}
|
||||
|
||||
async userUnlocked(userId: UserId): Promise<void> {
|
||||
if (
|
||||
(await this.pinStateService.getPinLockType(userId)) === "EPHEMERAL" &&
|
||||
!(await this.isPinDecryptionAvailable(userId))
|
||||
) {
|
||||
this.logService.info("[Pin Service] On first unlock: Setting up ephemeral PIN");
|
||||
|
||||
// On first unlock, set the ephemeral pin envelope, if it is not set yet
|
||||
const pin = await this.getPin(userId);
|
||||
await this.setPin(pin, "EPHEMERAL", userId);
|
||||
} else if ((await this.pinStateService.getPinLockType(userId)) === "PERSISTENT") {
|
||||
// Encrypted migration for persistent pin unlock to pin envelopes.
|
||||
// This will be removed at the earliest in 2026.1.0
|
||||
//
|
||||
// ----- ENCRYPTION MIGRATION -----
|
||||
// Pin-key encrypted user-keys are eagerly migrated to the new pin-protected user key envelope format.
|
||||
if ((await this.pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent(userId)) != null) {
|
||||
this.logService.info(
|
||||
"[Pin Service] Migrating legacy PIN key to PinProtectedUserKeyEnvelope",
|
||||
);
|
||||
const pin = await this.getPin(userId);
|
||||
await this.setPin(pin, "PERSISTENT", userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPin(userId: UserId): Promise<string> {
|
||||
assertNonNullish(userId, "userId");
|
||||
|
||||
const userKey: UserKey = await firstValueFromOrThrow(
|
||||
this.keyService.userKey$(userId),
|
||||
"userKey",
|
||||
);
|
||||
const userKeyEncryptedPin = await firstValueFromOrThrow(
|
||||
this.pinStateService.userKeyEncryptedPin$(userId),
|
||||
"userKeyEncryptedPin",
|
||||
);
|
||||
return this.encryptService.decryptString(userKeyEncryptedPin, userKey);
|
||||
}
|
||||
|
||||
async setPin(pin: string, pinLockType: PinLockType, userId: UserId): Promise<void> {
|
||||
assertNonNullish(pin, "pin");
|
||||
assertNonNullish(pinLockType, "pinLockType");
|
||||
assertNonNullish(userId, "userId");
|
||||
|
||||
// Use the sdk to create an enrollment, not yet persisting it to state
|
||||
const { pinProtectedUserKeyEnvelope, userKeyEncryptedPin } = await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
map((sdk) => {
|
||||
using ref = sdk.take();
|
||||
return ref.value.crypto().enroll_pin(pin);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await this.pinStateService.setPinState(
|
||||
userId,
|
||||
pinProtectedUserKeyEnvelope,
|
||||
userKeyEncryptedPin,
|
||||
pinLockType,
|
||||
);
|
||||
}
|
||||
|
||||
async unsetPin(userId: UserId): Promise<void> {
|
||||
assertNonNullish(userId, "userId");
|
||||
await this.pinStateService.clearPinState(userId);
|
||||
}
|
||||
|
||||
async isPinDecryptionAvailable(userId: UserId): Promise<boolean> {
|
||||
this.validateUserId(userId, "Cannot determine if decryption of user key via PIN is available.");
|
||||
|
||||
const pinLockType = await this.getPinLockType(userId);
|
||||
assertNonNullish(userId, "userId");
|
||||
|
||||
const pinLockType = await this.pinStateService.getPinLockType(userId);
|
||||
switch (pinLockType) {
|
||||
case "DISABLED":
|
||||
return false;
|
||||
case "PERSISTENT":
|
||||
// The above getPinLockType call ensures that we have either a PinKeyEncryptedUserKey set.
|
||||
// The above getPinLockType call ensures that we have either a PinKeyEncryptedUserKey or PinProtectedKeyEnvelope set.
|
||||
return true;
|
||||
case "EPHEMERAL": {
|
||||
// The above getPinLockType call ensures that we have a UserKeyEncryptedPin set.
|
||||
// However, we must additively check to ensure that we have a set PinKeyEncryptedUserKeyEphemeral b/c otherwise
|
||||
// we cannot take a PIN, derive a PIN key, and decrypt the ephemeral UserKey.
|
||||
const pinKeyEncryptedUserKeyEphemeral =
|
||||
await this.getPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
return Boolean(pinKeyEncryptedUserKeyEphemeral);
|
||||
// However, we must additively check to ensure that we have a set PinKeyEncryptedUserKeyEphemeral, since
|
||||
// this is only available after first unlock
|
||||
const ephemeralPinProtectedKeyEnvelope =
|
||||
await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, "EPHEMERAL");
|
||||
return ephemeralPinProtectedKeyEnvelope != null;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Compile-time check for exhaustive switch
|
||||
const _exhaustiveCheck: never = pinLockType;
|
||||
@@ -279,112 +141,89 @@ export class PinService implements PinServiceAbstraction {
|
||||
}
|
||||
|
||||
async decryptUserKeyWithPin(pin: string, userId: UserId): Promise<UserKey | null> {
|
||||
this.validateUserId(userId, "Cannot decrypt user key with PIN.");
|
||||
assertNonNullish(pin, "pin");
|
||||
assertNonNullish(userId, "userId");
|
||||
|
||||
try {
|
||||
const pinLockType = await this.getPinLockType(userId);
|
||||
const hasPinProtectedKeyEnvelopeSet =
|
||||
(await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, "EPHEMERAL")) != null ||
|
||||
(await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, "PERSISTENT")) != null;
|
||||
|
||||
const pinKeyEncryptedUserKey = await this.getPinKeyEncryptedKeys(pinLockType, userId);
|
||||
if (hasPinProtectedKeyEnvelopeSet) {
|
||||
this.logService.info("[Pin Service] Pin-unlock via PinProtectedUserKeyEnvelope");
|
||||
|
||||
const email = await firstValueFrom(
|
||||
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
|
||||
);
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
|
||||
const userKey: UserKey = await this.decryptUserKey(
|
||||
const pinLockType = await this.pinStateService.getPinLockType(userId);
|
||||
const envelope = await this.pinStateService.getPinProtectedUserKeyEnvelope(
|
||||
userId,
|
||||
pin,
|
||||
email,
|
||||
kdfConfig,
|
||||
pinKeyEncryptedUserKey,
|
||||
pinLockType,
|
||||
);
|
||||
if (!userKey) {
|
||||
this.logService.warning(`User key null after pin key decryption.`);
|
||||
|
||||
try {
|
||||
// Use the sdk to create an enrollment, not yet persisting it to state
|
||||
const startTime = performance.now();
|
||||
const userKeyBytes = await firstValueFrom(
|
||||
this.sdkService.client$.pipe(
|
||||
map((sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
return sdk.crypto().unseal_password_protected_key_envelope(pin, envelope!);
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.logService.measure(startTime, "Crypto", "PinService", "UnsealPinEnvelope");
|
||||
|
||||
return new SymmetricCryptoKey(userKeyBytes) as UserKey;
|
||||
} catch (error) {
|
||||
this.logService.error(`Failed to unseal pin: ${error}`);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
this.logService.info("[Pin Service] Pin-unlock via legacy PinKeyEncryptedUserKey");
|
||||
|
||||
if (!(await this.validatePin(userKey, pin, userId))) {
|
||||
this.logService.warning(`Pin key decryption successful but pin validation failed.`);
|
||||
// This branch is deprecated and will be removed in the future, but is kept for migration.
|
||||
try {
|
||||
const pinKeyEncryptedUserKey =
|
||||
await this.pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent(userId);
|
||||
const email = await firstValueFrom(
|
||||
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
|
||||
);
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
return await this.decryptUserKey(pin, email, kdfConfig, pinKeyEncryptedUserKey!);
|
||||
} catch (error) {
|
||||
this.logService.error(`Error decrypting user key with pin: ${error}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return userKey;
|
||||
} catch (error) {
|
||||
this.logService.error(`Error decrypting user key with pin: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Anything below here is deprecated and will be removed subsequently
|
||||
|
||||
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
|
||||
const startTime = performance.now();
|
||||
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
|
||||
this.logService.measure(startTime, "Crypto", "PinService", "makePinKey");
|
||||
|
||||
return (await this.keyGenerationService.stretchKey(pinKey)) as PinKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the UserKey with the provided PIN.
|
||||
* @deprecated
|
||||
* @throws If the PIN does not match the PIN that was used to encrypt the user key
|
||||
* @throws If the salt, or KDF don't match the salt / KDF used to encrypt the user key
|
||||
*/
|
||||
private async decryptUserKey(
|
||||
userId: UserId,
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdfConfig: KdfConfig,
|
||||
pinKeyEncryptedUserKey?: EncString,
|
||||
pinKeyEncryptedUserKey: EncString,
|
||||
): Promise<UserKey> {
|
||||
this.validateUserId(userId, "Cannot decrypt user key.");
|
||||
|
||||
pinKeyEncryptedUserKey ||= await this.getPinKeyEncryptedUserKeyPersistent(userId);
|
||||
pinKeyEncryptedUserKey ||= await this.getPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
|
||||
if (!pinKeyEncryptedUserKey) {
|
||||
throw new Error("No pinKeyEncryptedUserKey found.");
|
||||
}
|
||||
|
||||
assertNonNullish(pin, "pin");
|
||||
assertNonNullish(salt, "salt");
|
||||
assertNonNullish(kdfConfig, "kdfConfig");
|
||||
assertNonNullish(pinKeyEncryptedUserKey, "pinKeyEncryptedUserKey");
|
||||
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
|
||||
const userKey = await this.encryptService.unwrapSymmetricKey(pinKeyEncryptedUserKey, pinKey);
|
||||
|
||||
return userKey as UserKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's `pinKeyEncryptedUserKey` (persistent or ephemeral)
|
||||
* (if one exists) based on the user's PinLockType.
|
||||
*
|
||||
* @throws If PinLockType is 'DISABLED' or if userId is not provided
|
||||
*/
|
||||
private async getPinKeyEncryptedKeys(
|
||||
pinLockType: PinLockType,
|
||||
userId: UserId,
|
||||
): Promise<EncString> {
|
||||
this.validateUserId(userId, "Cannot get PinKey encrypted keys.");
|
||||
|
||||
switch (pinLockType) {
|
||||
case "PERSISTENT": {
|
||||
return await this.getPinKeyEncryptedUserKeyPersistent(userId);
|
||||
}
|
||||
case "EPHEMERAL": {
|
||||
return await this.getPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
}
|
||||
case "DISABLED":
|
||||
throw new Error("Pin is disabled");
|
||||
default: {
|
||||
// Compile-time check for exhaustive switch
|
||||
const _exhaustiveCheck: never = pinLockType;
|
||||
return _exhaustiveCheck;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async validatePin(userKey: UserKey, pin: string, userId: UserId): Promise<boolean> {
|
||||
this.validateUserId(userId, "Cannot validate PIN.");
|
||||
|
||||
const userKeyEncryptedPin = await this.getUserKeyEncryptedPin(userId);
|
||||
const decryptedPin = await this.encryptService.decryptString(userKeyEncryptedPin, userKey);
|
||||
|
||||
const isPinValid = this.cryptoFunctionService.compareFast(decryptedPin, pin);
|
||||
return isPinValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a custom error message if user ID is not provided.
|
||||
*/
|
||||
private validateUserId(userId: UserId, errorMessage: string = "") {
|
||||
if (!userId) {
|
||||
throw new Error(`User ID is required. ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user