1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-06 18:57:56 +01:00

more crypto service refactors

- check for auto key when getting user key
- consolidate getUserKeyFromMemory and FromStorage methods
- move bio key references out of base crypto service
- update either pin key when setting user key instead of lock component
- group deprecated methods
- rename key legacy method
This commit is contained in:
Jacob Fink 2023-06-27 18:53:40 -04:00
parent 2efec1d880
commit 0debdc5514
No known key found for this signature in database
GPG Key ID: C2F7ACF05859D008
28 changed files with 248 additions and 211 deletions

View File

@ -93,7 +93,7 @@ export class LockComponent extends BaseLockComponent {
}, 100);
}
async unlockBiometric(): Promise<boolean> {
override async unlockBiometric(): Promise<boolean> {
if (!this.biometricLock) {
return;
}

View File

@ -7,10 +7,22 @@ import {
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
export class BrowserCryptoService extends CryptoService {
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
if (keySuffix === KeySuffixOptions.Biometric) {
return await this.stateService.getBiometricUnlock({ userId: userId });
}
return super.hasUserKeyStored(keySuffix, userId);
}
/**
* Browser doesn't store biometric keys, so we retrieve them from the desktop and return
* if we successfully saved it into memory as the User Key
*/
protected override async getKeyFromStorage(keySuffix: KeySuffixOptions): Promise<UserKey> {
if (keySuffix === KeySuffixOptions.Biometric) {
await this.platformUtilService.authenticateBiometric();
const userKey = await this.getUserKeyFromMemory();
// this will check for an auto key, but that is acceptable
const userKey = await this.getUserKey();
if (userKey) {
return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey.keyB64).buffer) as UserKey;
}

View File

@ -269,7 +269,6 @@ export class SettingsComponent implements OnInit {
this.form.controls.pin.setValue(await ref.onClosedPromise());
} else {
await this.cryptoService.clearPinProtectedKey();
await this.vaultTimeoutSettingsService.clear();
}
}
@ -324,7 +323,7 @@ export class SettingsComponent implements OnInit {
});
await this.stateService.setBiometricAwaitingAcceptance(true);
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
await Promise.race([
submitted.then(async (result) => {

View File

@ -568,7 +568,7 @@ export class LoginCommand {
const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newMasterKey);
// Grab user key
const userKey = await this.cryptoService.getUserKeyFromMemory();
const userKey = await this.cryptoService.getUserKey();
if (!userKey) {
throw new Error("User key not found.");
}

View File

@ -5,7 +5,6 @@ import * as koa from "koa";
import * as koaBodyParser from "koa-bodyparser";
import * as koaJson from "koa-json";
import { KeySuffixOptions } from "@bitwarden/common/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ConfirmCommand } from "../admin-console/commands/confirm.command";
@ -425,14 +424,7 @@ export class ServeCommand {
this.processResponse(res, Response.error("You are not logged in."));
return true;
}
if (await this.main.cryptoService.hasUserKeyInMemory()) {
return false;
} else if (await this.main.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto)) {
// load key into memory
const userAutoKey = await this.main.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Auto
);
await this.main.cryptoService.setUserKey(userAutoKey);
if (await this.main.cryptoService.hasUserKey()) {
return false;
}
this.processResponse(res, Response.error("Vault is locked."));

View File

@ -2,7 +2,6 @@ import * as chalk from "chalk";
import * as program from "commander";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { KeySuffixOptions } from "@bitwarden/common/enums";
import { LockCommand } from "./auth/commands/lock.command";
import { LoginCommand } from "./auth/commands/login.command";
@ -597,14 +596,8 @@ export class Program {
protected async exitIfLocked() {
await this.exitIfNotAuthed();
if (await this.main.cryptoService.hasUserKeyInMemory()) {
if (await this.main.cryptoService.hasUserKey()) {
return;
} else if (await this.main.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto)) {
// load key into memory
const userAutoKey = await this.main.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Auto
);
await this.main.cryptoService.setUserKey(userAutoKey);
} else if (process.env.BW_NOINTERACTION !== "true") {
// must unlock
if (await this.main.keyConnectorService.getUsesKeyConnector()) {

View File

@ -126,7 +126,7 @@ export class CreateCommand {
return Response.error("Premium status is required to use this feature.");
}
const userKey = await this.cryptoService.getUserKeyFromMemory();
const userKey = await this.cryptoService.getUserKey();
if (userKey == null) {
return Response.error(
"You must update your encryption key before you can use this feature. " +

View File

@ -374,7 +374,6 @@ export class SettingsComponent implements OnInit {
this.form.controls.pin.setValue(await ref.onClosedPromise());
}
if (!this.form.value.pin) {
await this.cryptoService.clearPinProtectedKey();
await this.vaultTimeoutSettingsService.clear();
}
}
@ -388,7 +387,7 @@ export class SettingsComponent implements OnInit {
if (!this.form.value.biometric || !this.supportsBiometric) {
this.form.controls.biometric.setValue(false);
await this.stateService.setBiometricUnlock(null);
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
return;
}
@ -401,7 +400,7 @@ export class SettingsComponent implements OnInit {
await this.stateService.setBiometricRequirePasswordOnStart(true);
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
}
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
// Validate the key is stored in case biometrics fail.
const biometricSet = await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric);
@ -435,7 +434,7 @@ export class SettingsComponent implements OnInit {
await this.stateService.setBiometricEncryptionClientKeyHalf(null);
}
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
}
async saveFavicons() {

View File

@ -26,6 +26,23 @@ export class ElectronCryptoService extends CryptoService {
super(cryptoFunctionService, encryptService, platformUtilsService, logService, stateService);
}
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
if (keySuffix === KeySuffixOptions.Biometric) {
const oldKey = await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId });
return oldKey || (await this.stateService.hasUserKeyBiometric({ userId: userId }));
}
return super.hasUserKeyStored(keySuffix, userId);
}
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void> {
if (keySuffix === KeySuffixOptions.Biometric) {
this.stateService.setUserKeyBiometric(null, { userId: userId });
this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
return;
}
super.clearStoredUserKey(keySuffix, userId);
}
protected override async storeAdditionalKeys(key: UserKey, userId?: string) {
await super.storeAdditionalKeys(key, userId);
@ -36,7 +53,7 @@ export class ElectronCryptoService extends CryptoService {
} else {
await this.stateService.setUserKeyBiometric(null, { userId: userId });
}
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
}
protected override async getKeyFromStorage(
@ -70,13 +87,18 @@ export class ElectronCryptoService extends CryptoService {
return await super.shouldStoreKey(keySuffix, userId);
}
protected override async clearAllStoredUserKeys(userId?: string): Promise<void> {
await this.stateService.setUserKeyBiometric(null, { userId: userId });
super.clearAllStoredUserKeys(userId);
}
private async getBiometricEncryptionClientKeyHalf(userId?: string): Promise<CsprngString | null> {
try {
let biometricKey = await this.stateService
.getBiometricEncryptionClientKeyHalf({ userId })
.then((result) => result?.decrypt(null /* user encrypted */))
.then((result) => result as CsprngString);
const userKey = await this.getKeyForUserEncryption();
const userKey = await this.getUserKeyWithLegacySupport();
if (biometricKey == null && userKey != null) {
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
@ -90,6 +112,18 @@ export class ElectronCryptoService extends CryptoService {
}
}
// --LEGACY METHODS--
// We previously used the master key for additional keys, but now we use the user key.
// These methods support migrating the old keys to the new ones.
override async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string) {
if (keySuffix === KeySuffixOptions.Biometric) {
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
}
super.clearDeprecatedKeys(keySuffix, userId);
}
private async migrateBiometricKeyIfNeeded(userId?: string) {
if (await this.stateService.hasCryptoMasterKeyBiometric({ userId })) {
const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId });

View File

@ -58,7 +58,7 @@ export class EnrollMasterPasswordReset {
const publicKey = Utils.fromB64ToArray(orgKeys.publicKey);
// RSA Encrypt user's encKey.key with organization public key
const userKey = await this.cryptoService.getUserKeyFromMemory();
const userKey = await this.cryptoService.getUserKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey.buffer);
keyString = encryptedKey.encryptedString;
toastStringRef = "enrollPasswordResetSuccess";

View File

@ -140,7 +140,7 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user's encKey.key with organization public key
const userKey = await this.cryptoService.getUserKeyFromMemory();
const userKey = await this.cryptoService.getUserKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey.buffer);
// Add reset password key to accept request

View File

@ -12,7 +12,6 @@ import {
EmergencyAccessGranteeDetailsResponse,
EmergencyAccessGrantorDetailsResponse,
} from "@bitwarden/common/auth/models/response/emergency-access.response";
import { KeySuffixOptions } from "@bitwarden/common/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -303,8 +302,7 @@ export class EmergencyAccessComponent implements OnInit {
// Encrypt the user key with the grantees public key, and send it to bitwarden for escrow.
private async doConfirmation(details: EmergencyAccessGranteeDetailsResponse) {
let userKey = await this.cryptoService.getUserKeyFromMemory();
userKey ||= await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Auto);
const userKey = await this.cryptoService.getUserKey();
if (!userKey) {
throw new Error("No user key found");
}

View File

@ -93,7 +93,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
);
let newProtectedUserKey: [UserKey, EncString] = null;
const userKey = await this.cryptoService.getUserKeyFromMemory();
const userKey = await this.cryptoService.getUserKey();
if (userKey == null) {
newProtectedUserKey = await this.cryptoService.makeUserKey(newMasterKey);
} else {

View File

@ -287,17 +287,6 @@ export class LockComponent implements OnInit, OnDestroy {
}
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
// if MP on restart is enabled, use it to get the PIN and store the ephemeral
// pin protected user key
if (this.pinStatus === `ENABLED_WITH_MP_ON_RESET`) {
const protectedPin = await this.stateService.getProtectedPin();
const pin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), userKey);
const pinKey = await this.cryptoService.makePinKey(pin, this.email, kdf, kdfConfig);
await this.stateService.setUserKeyPinEphemeral(
await this.cryptoService.encrypt(userKey.key, pinKey)
);
}
await this.setKeyAndContinue(userKey, true);
}

View File

@ -126,7 +126,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
);
// Grab user key
const userKey = await this.cryptoService.getUserKeyFromMemory();
const userKey = await this.cryptoService.getUserKey();
// Encrypt user key with new master key
const newProtectedUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(

View File

@ -132,7 +132,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user key with organization public key
const userKey = await this.cryptoService.getUserKeyFromMemory();
const userKey = await this.cryptoService.getUserKey();
const encryptedUserKey = await this.cryptoService.rsaEncrypt(
userKey.key,
publicKey.buffer

View File

@ -1,6 +1,7 @@
import { Directive, OnInit } from "@angular/core";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { KeySuffixOptions } from "@bitwarden/common/enums/key-suffix-options.enum";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -41,7 +42,7 @@ export class SetPinComponent implements OnInit {
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig()
);
const userKey = await this.cryptoService.getUserKeyFromMemory();
const userKey = await this.cryptoService.getUserKey();
const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey);
const encPin = await this.cryptoService.encrypt(this.pin, userKey);
await this.stateService.setProtectedPin(encPin.encryptedString);
@ -50,7 +51,7 @@ export class SetPinComponent implements OnInit {
} else {
await this.stateService.setUserKeyPin(pinProtectedKey);
}
await this.cryptoService.clearDeprecatedPinKeys();
await this.cryptoService.clearDeprecatedKeys(KeySuffixOptions.Pin);
this.modalRef.close(true);
}

View File

@ -15,45 +15,44 @@ import {
} from "../models/domain/symmetric-crypto-key";
export abstract class CryptoService {
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
*/
getKeyForUserEncryption: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
/**
* Sets the provided user key and stores
* any other necessary versions, such as biometrics
* any other necessary versions (such as auto, biometrics,
* or pin)
* @param key The user key to set
* @param userId The desired user
*/
setUserKey: (key: UserKey) => Promise<void>;
/**
* Gets the user key from memory and sets it again,
* kicking off a refresh of any additional keys that are needed.
* kicking off a refresh of any additional keys
* (such as auto, biometrics, or pin)
*/
toggleKey: () => Promise<void>;
refreshAdditionalKeys: () => Promise<void>;
/**
* Retrieves the user key
* @param keySuffix The desired version of the user's key to retrieve
* from storage if it is not available in memory
* @param userId The desired user
* @returns The user key
*/
getUserKeyFromMemory: (userId?: string) => Promise<UserKey>;
getUserKey: (userId?: string) => Promise<UserKey>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
* @param userId The desired user
*/
getUserKeyWithLegacySupport: (userId?: string) => Promise<UserKey>;
/**
* Retrieves the user key from storage
* @param keySuffix The desired version of the user's key to retrieve
* @param userId The desired user
* @returns The user key
*/
getUserKeyFromStorage: (
keySuffix: KeySuffixOptions.Auto | KeySuffixOptions.Biometric,
userId?: string
) => Promise<UserKey>;
getUserKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<UserKey>;
/**
* @returns True if any version of the user key is available
* @returns True if the user key is available
*/
hasUserKey: () => Promise<boolean>;
/**
@ -66,10 +65,7 @@ export abstract class CryptoService {
* @param userId The desired user
* @returns True if the provided version of the user key is stored
*/
hasUserKeyStored: (
keySuffix?: KeySuffixOptions.Auto | KeySuffixOptions.Biometric,
userId?: string
) => Promise<boolean>;
hasUserKeyStored: (keySuffix: KeySuffixOptions, userId?: string) => Promise<boolean>;
/**
* Generates a new user key
* @param masterKey The user's master key
@ -285,15 +281,12 @@ export abstract class CryptoService {
*/
makePinKey: (pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig) => Promise<PinKey>;
/**
* Clears the user's pin protected user key
* Clears the user's pin keys from storage
* Note: This will remove the stored pin and as a result,
* disable pin protection for the user
* @param userId The desired user
*/
clearPinProtectedKey: (userId?: string) => Promise<void>;
/**
* Clears the user's old pin keys from storage
* @param userId The desired user
*/
clearDeprecatedPinKeys: (userId?: string) => Promise<void>;
clearPinKeys: (userId?: string) => Promise<void>;
/**
* Decrypts the user key with their pin
* @param pin The user's PIN
@ -347,6 +340,14 @@ export abstract class CryptoService {
kdfConfig: KdfConfig,
protectedKeyCs?: EncString
) => Promise<MasterKey>;
/**
* Previously, the master key was used for any additional key like the biometrics or pin key.
* We have switched to using the user key for these purposes. This method is for clearing the state
* of the older keys on logout or post migration.
* @param keySuffix The desired type of key to clear
* @param userId The desired user
*/
clearDeprecatedKeys: (keySuffix: KeySuffixOptions, userId?: string) => Promise<void>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.encrypt

View File

@ -225,7 +225,7 @@ describe("EncString", () => {
await encString.decrypt(null, key);
expect(cryptoService.getKeyForUserEncryption).not.toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
});
@ -243,11 +243,11 @@ describe("EncString", () => {
it("gets the user's decryption key if required", async () => {
const userKey = mock<SymmetricCryptoKey>();
cryptoService.getKeyForUserEncryption.mockResolvedValue(userKey);
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
await encString.decrypt(null, null);
expect(cryptoService.getKeyForUserEncryption).toHaveBeenCalledWith();
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, userKey);
});
});

View File

@ -162,7 +162,7 @@ export class EncString implements Encrypted {
const cryptoService = Utils.getContainerService().getCryptoService();
return orgId != null
? await cryptoService.getOrgKey(orgId)
: await cryptoService.getKeyForUserEncryption();
: await cryptoService.getUserKeyWithLegacySupport();
}
}

View File

@ -61,7 +61,7 @@ describe("cryptoService", () => {
it("returns the user key if available", async () => {
stateSvcGetUserKey.mockResolvedValue(mockUserKey);
const encryptionKey = await cryptoService.getKeyForUserEncryption();
const encryptionKey = await cryptoService.getUserKeyWithLegacySupport();
expect(stateSvcGetUserKey).toHaveBeenCalled();
expect(stateSvcGetMasterKey).not.toHaveBeenCalled();
@ -73,7 +73,7 @@ describe("cryptoService", () => {
stateSvcGetUserKey.mockResolvedValue(null);
stateSvcGetMasterKey.mockResolvedValue(mockMasterKey);
const encryptionKey = await cryptoService.getKeyForUserEncryption();
const encryptionKey = await cryptoService.getUserKeyWithLegacySupport();
expect(stateSvcGetMasterKey).toHaveBeenCalled();
expect(encryptionKey).toEqual(mockMasterKey);

View File

@ -44,53 +44,57 @@ export class CryptoService implements CryptoServiceAbstraction {
protected stateService: StateService
) {}
async getKeyForUserEncryption(): Promise<SymmetricCryptoKey> {
const userKey = await this.getUserKeyFromMemory();
if (userKey != null) {
return userKey;
}
// Legacy support: encryption used to be done with the master key (derived from master password).
// Users who have not migrated will have a null user key and must use the master key instead.
return await this.getMasterKey();
}
async setUserKey(key: UserKey, userId?: string): Promise<void> {
await this.stateService.setUserKey(key, { userId: userId });
await this.storeAdditionalKeys(key, userId);
}
async toggleKey(): Promise<void> {
const key = await this.getUserKeyFromMemory();
async refreshAdditionalKeys(): Promise<void> {
const key = await this.getUserKey();
await this.setUserKey(key);
}
async getUserKeyFromMemory(userId?: string): Promise<UserKey> {
return await this.stateService.getUserKey({ userId: userId });
}
async getUserKeyFromStorage(
keySuffix: KeySuffixOptions.Auto | KeySuffixOptions.Biometric,
userId?: string
): Promise<UserKey> {
const userKey = await this.getKeyFromStorage(keySuffix, userId);
if (userKey != null) {
if (!(await this.validateUserKey(userKey))) {
this.logService.warning("Wrong key, throwing away stored key");
await this.clearAllStoredUserKeys(userId);
return null;
}
async getUserKey(userId?: string): Promise<UserKey> {
let userKey = await this.stateService.getUserKey({ userId: userId });
if (userKey) {
return userKey;
}
// If the user has set their vault timeout to 'Never', we can load the user key from storage
if (await this.hasUserKeyStored(KeySuffixOptions.Auto)) {
userKey = await this.getKeyFromStorage(KeySuffixOptions.Auto, userId);
if (userKey) {
await this.setUserKey(userKey, userId);
return userKey;
}
}
}
async getUserKeyWithLegacySupport(userId?: string): Promise<UserKey> {
const userKey = await this.getUserKey(userId);
if (userKey) {
return userKey;
}
// Legacy support: encryption used to be done with the master key (derived from master password).
// Users who have not migrated will have a null user key and must use the master key instead.
return (await this.getMasterKey(userId)) as any as UserKey;
}
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise<UserKey> {
const userKey = await this.getKeyFromStorage(keySuffix, userId);
if (userKey) {
if (!(await this.validateUserKey(userKey))) {
this.logService.warning("Invalid key, throwing away stored keys");
await this.clearAllStoredUserKeys(userId);
}
return userKey;
}
return null;
}
async hasUserKey(): Promise<boolean> {
return (
(await this.hasUserKeyInMemory()) ||
(await this.hasUserKeyStored(KeySuffixOptions.Auto)) ||
(await this.hasUserKeyStored(KeySuffixOptions.Biometric))
(await this.hasUserKeyInMemory()) || (await this.hasUserKeyStored(KeySuffixOptions.Auto))
);
}
@ -98,14 +102,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return (await this.stateService.getUserKey({ userId: userId })) != null;
}
async hasUserKeyStored(
keySuffix: KeySuffixOptions.Auto | KeySuffixOptions.Biometric,
userId?: string
): Promise<boolean> {
if (keySuffix === KeySuffixOptions.Biometric) {
const oldKey = await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId });
return oldKey || (await this.stateService.hasUserKeyBiometric({ userId: userId }));
}
async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
return (await this.getKeyFromStorage(keySuffix, userId)) != null;
}
@ -127,16 +124,13 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void> {
switch (keySuffix) {
case KeySuffixOptions.Auto:
this.stateService.setUserKeyAuto(null, { userId: userId });
break;
case KeySuffixOptions.Biometric:
this.stateService.setUserKeyBiometric(null, { userId: userId });
break;
case KeySuffixOptions.Pin:
this.stateService.setUserKeyPinEphemeral(null, { userId: userId });
break;
if (keySuffix === KeySuffixOptions.Auto) {
this.stateService.setUserKeyAuto(null, { userId: userId });
this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
}
if (keySuffix === KeySuffixOptions.Pin) {
this.stateService.setUserKeyPinEphemeral(null, { userId: userId });
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
}
}
@ -174,7 +168,7 @@ export class CryptoService implements CryptoServiceAbstraction {
masterKey: MasterKey,
userKey?: UserKey
): Promise<[UserKey, EncString]> {
userKey ||= await this.getUserKeyFromMemory();
userKey ||= await this.getUserKey();
return this.buildProtectedSymmetricKey(masterKey, userKey.key);
}
@ -463,7 +457,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const privateKey = await this.encryptService.decryptToBytes(
new EncString(encPrivateKey),
await this.getKeyForUserEncryption()
await this.getUserKeyWithLegacySupport()
);
await this.stateService.setDecryptedPrivateKey(privateKey);
return privateKey;
@ -487,7 +481,7 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> {
key ||= await this.getUserKeyFromMemory();
key ||= await this.getUserKey();
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
@ -511,14 +505,11 @@ export class CryptoService implements CryptoServiceAbstraction {
return (await this.stretchKey(pinKey)) as PinKey;
}
async clearPinProtectedKey(userId?: string): Promise<void> {
async clearPinKeys(userId?: string): Promise<void> {
await this.stateService.setUserKeyPin(null, { userId: userId });
await this.clearDeprecatedPinKeys(userId);
}
async clearDeprecatedPinKeys(userId?: string): Promise<void> {
await this.stateService.setEncryptedPinProtected(null, { userId: userId });
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
await this.stateService.setUserKeyPinEphemeral(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId });
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
}
async decryptUserKeyWithPin(
@ -529,6 +520,7 @@ export class CryptoService implements CryptoServiceAbstraction {
pinProtectedUserKey?: EncString
): Promise<UserKey> {
pinProtectedUserKey ||= await this.stateService.getUserKeyPin();
pinProtectedUserKey ||= await this.stateService.getUserKeyPinEphemeral();
if (!pinProtectedUserKey) {
throw new Error("No PIN protected key found.");
}
@ -573,7 +565,7 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.clearOrgKeys(false, userId);
await this.clearProviderKeys(false, userId);
await this.clearKeyPair(false, userId);
await this.clearPinProtectedKey(userId);
await this.clearPinKeys(userId);
}
async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise<EncString> {
@ -702,8 +694,10 @@ export class CryptoService implements CryptoServiceAbstraction {
}
/**
* Regenerates any additional keys if needed. Useful to make sure
* other keys stay in sync when the user key has been rotated.
* Generates any additional keys if needed. Additional keys are
* keys such as biometrics, auto, and pin keys.
* Useful to make sure other keys stay in sync when the user key
* has been rotated.
* @param key The user key
* @param userId The desired user
*/
@ -714,27 +708,43 @@ export class CryptoService implements CryptoServiceAbstraction {
} else {
await this.stateService.setUserKeyAuto(null, { userId: userId });
}
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
await this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
if (storePin) {
await this.storePinKey(key);
await this.clearDeprecatedPinKeys(userId);
// We can't always clear deprecated keys because the pin is only
// migrated once used to unlock
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
} else {
await this.stateService.setUserKeyPin(null, { userId: userId });
await this.stateService.setUserKeyPinEphemeral(null, { userId: userId });
}
}
/**
* Stores the pin key if needed. If MP on Reset is enabled, stores the
* ephemeral version.
* @param key The user key
*/
protected async storePinKey(key: UserKey) {
const email = await this.stateService.getEmail();
const kdf = await this.stateService.getKdfType();
const kdfConfig = await this.stateService.getKdfConfig();
const pin = await this.encryptService.decryptToUtf8(
new EncString(await this.stateService.getProtectedPin()),
key
);
const pinKey = await this.makePinKey(pin, email, kdf, kdfConfig);
await this.stateService.setUserKeyPin(await this.encryptService.encrypt(key.key, pinKey));
const pinKey = await this.makePinKey(
pin,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig()
);
const encPin = await this.encryptService.encrypt(key.key, pinKey);
if ((await this.stateService.getUserKeyPin()) != null) {
await this.stateService.setUserKeyPin(encPin);
} else {
await this.stateService.setUserKeyPinEphemeral(encPin);
}
}
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string) {
@ -747,12 +757,7 @@ export class CryptoService implements CryptoServiceAbstraction {
}
case KeySuffixOptions.Pin: {
const protectedPin = await this.stateService.getProtectedPin({ userId: userId });
// This could cause a possible timing issue. Need to make sure the ephemeral key is set before
// we set our user key
const userKeyPinEphemeral = await this.stateService.getUserKeyPinEphemeral({
userId: userId,
});
shouldStoreKey = !!protectedPin && !userKeyPinEphemeral;
shouldStoreKey = !!protectedPin;
break;
}
}
@ -773,21 +778,9 @@ export class CryptoService implements CryptoServiceAbstraction {
return null;
}
private async migrateAutoKeyIfNeeded(userId?: string) {
const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId });
if (oldAutoKey) {
// decrypt
const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(oldAutoKey).buffer
) as MasterKey;
const userKey = await this.decryptUserKeyWithMasterKey(
masterKey,
new EncString(await this.stateService.getEncryptedCryptoSymmetricKey())
);
// migrate
await this.stateService.setUserKeyAuto(userKey.keyB64, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
}
protected async clearAllStoredUserKeys(userId?: string): Promise<void> {
await this.stateService.setUserKeyAuto(null, { userId: userId });
await this.stateService.setUserKeyPinEphemeral(null, { userId: userId });
}
private async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
@ -835,13 +828,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return [new SymmetricCryptoKey(newSymKey) as T, protectedSymKey];
}
private async clearAllStoredUserKeys(userId?: string): Promise<void> {
await this.stateService.setUserKeyAuto(null, { userId: userId });
await this.stateService.setUserKeyBiometric(null, { userId: userId });
await this.stateService.setUserKeyPinEphemeral(null, { userId: userId });
}
async makeKey(
private async makeKey(
password: string,
salt: string,
kdf: KdfType,
@ -890,12 +877,44 @@ export class CryptoService implements CryptoServiceAbstraction {
return new SymmetricCryptoKey(key);
}
// --LEGACY METHODS--
// We previously used the master key for additional keys, but now we use the user key.
// These methods support migrating the old keys to the new ones.
async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string) {
if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
} else if (keySuffix === KeySuffixOptions.Pin) {
await this.stateService.setEncryptedPinProtected(null, { userId: userId });
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
}
}
private async migrateAutoKeyIfNeeded(userId?: string) {
const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId });
if (oldAutoKey) {
// decrypt
const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(oldAutoKey).buffer
) as MasterKey;
const userKey = await this.decryptUserKeyWithMasterKey(
masterKey,
new EncString(await this.stateService.getEncryptedCryptoSymmetricKey())
);
// migrate
await this.stateService.setUserKeyAuto(userKey.keyB64, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
}
}
// --DEPRECATED METHODS--
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.encrypt
*/
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
key ||= await this.getKeyForUserEncryption();
key ||= await this.getUserKeyWithLegacySupport();
return await this.encryptService.encrypt(plainValue, key);
}
@ -904,7 +923,7 @@ export class CryptoService implements CryptoServiceAbstraction {
* and then call encryptService.encryptToBytes
*/
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
key ||= await this.getKeyForUserEncryption();
key ||= await this.getUserKeyWithLegacySupport();
return this.encryptService.encryptToBytes(plainValue, key);
}
@ -913,7 +932,7 @@ export class CryptoService implements CryptoServiceAbstraction {
* and then call encryptService.decryptToBytes
*/
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
key ||= await this.getKeyForUserEncryption();
key ||= await this.getUserKeyWithLegacySupport();
return this.encryptService.decryptToBytes(encString, key);
}
@ -922,7 +941,7 @@ export class CryptoService implements CryptoServiceAbstraction {
* and then call encryptService.decryptToUtf8
*/
async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string> {
key ||= await this.getKeyForUserEncryption();
key ||= await this.getUserKeyWithLegacySupport();
return await this.encryptService.decryptToUtf8(encString, key);
}
@ -935,7 +954,7 @@ export class CryptoService implements CryptoServiceAbstraction {
throw new Error("No buffer provided for decryption.");
}
key ||= await this.getKeyForUserEncryption();
key ||= await this.getUserKeyWithLegacySupport();
return this.encryptService.decryptToBytes(encBuffer, key);
}

View File

@ -25,7 +25,7 @@ export class DeviceCryptoService implements DeviceCryptoServiceAbstraction {
async trustDevice(): Promise<DeviceResponse> {
// Attempt to get user key
const userKey: UserKey = await this.cryptoService.getUserKeyFromMemory();
const userKey: UserKey = await this.cryptoService.getUserKey();
// If user key is not found, throw error
if (!userKey) {

View File

@ -151,7 +151,7 @@ describe("deviceCryptoService", () => {
let makeDeviceKeySpy: jest.SpyInstance;
let rsaGenerateKeyPairSpy: jest.SpyInstance;
let cryptoSvcGetUserKeyFromMemorySpy: jest.SpyInstance;
let cryptoSvcGetUserKeySpy: jest.SpyInstance;
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
let encryptServiceEncryptSpy: jest.SpyInstance;
let appIdServiceGetAppIdSpy: jest.SpyInstance;
@ -198,8 +198,8 @@ describe("deviceCryptoService", () => {
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
.mockResolvedValue(mockDeviceRsaKeyPair);
cryptoSvcGetUserKeyFromMemorySpy = jest
.spyOn(cryptoService, "getUserKeyFromMemory")
cryptoSvcGetUserKeySpy = jest
.spyOn(cryptoService, "getUserKey")
.mockResolvedValue(mockUserKey);
cryptoSvcRsaEncryptSpy = jest
@ -231,7 +231,7 @@ describe("deviceCryptoService", () => {
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcGetUserKeyFromMemorySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcGetUserKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
@ -251,17 +251,17 @@ describe("deviceCryptoService", () => {
it("throws specific error if user key is not found", async () => {
// setup the spy to return null
cryptoSvcGetUserKeyFromMemorySpy.mockResolvedValue(null);
cryptoSvcGetUserKeySpy.mockResolvedValue(null);
// check if the expected error is thrown
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
// reset the spy
cryptoSvcGetUserKeyFromMemorySpy.mockReset();
cryptoSvcGetUserKeySpy.mockReset();
// setup the spy to return undefined
cryptoSvcGetUserKeyFromMemorySpy.mockResolvedValue(undefined);
cryptoSvcGetUserKeySpy.mockResolvedValue(undefined);
// check if the expected error is thrown
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
@ -280,9 +280,9 @@ describe("deviceCryptoService", () => {
errorText: "rsaGenerateKeyPair error",
},
{
method: "getUserKeyFromMemory",
spy: () => cryptoSvcGetUserKeyFromMemorySpy,
errorText: "getUserKeyFromMemory error",
method: "getUserKey",
spy: () => cryptoSvcGetUserKeySpy,
errorText: "getUserKey error",
},
{
method: "rsaEncrypt",

View File

@ -2,6 +2,7 @@ import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction }
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../admin-console/enums";
import { TokenService } from "../../auth/abstractions/token.service";
import { KeySuffixOptions } from "../../enums/key-suffix-options.enum";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service";
@ -43,7 +44,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
await this.tokenService.setClientId(clientId);
await this.tokenService.setClientSecret(clientSecret);
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
}
async isPinLockSet(): Promise<PinLockType> {
@ -116,8 +117,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
async clear(userId?: string): Promise<void> {
await this.stateService.setEverBeenUnlocked(false, { userId: userId });
await this.stateService.setUserKeyPinEphemeral(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId });
await this.cryptoService.clearDeprecatedPinKeys(userId);
await this.cryptoService.clearPinKeys(userId);
await this.cryptoService.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
}
}

View File

@ -105,7 +105,7 @@ describe("Attachment", () => {
await attachment.decrypt(null, providedKey);
expect(cryptoService.getKeyForUserEncryption).not.toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey);
});
@ -121,11 +121,11 @@ describe("Attachment", () => {
it("gets the user's decryption key if required", async () => {
const userKey = mock<SymmetricCryptoKey>();
cryptoService.getKeyForUserEncryption.mockResolvedValue(userKey);
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
await attachment.decrypt(null, null);
expect(cryptoService.getKeyForUserEncryption).toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey);
});
});

View File

@ -71,7 +71,7 @@ export class Attachment extends Domain {
const cryptoService = Utils.getContainerService().getCryptoService();
return orgId != null
? await cryptoService.getOrgKey(orgId)
: await cryptoService.getKeyForUserEncryption();
: await cryptoService.getUserKeyWithLegacySupport();
}
toAttachmentData(): AttachmentData {

View File

@ -336,7 +336,7 @@ export class CipherService implements CipherServiceAbstraction {
const ciphers = await this.getAll();
const orgKeys = await this.cryptoService.getOrgKeys();
const userKey = await this.cryptoService.getKeyForUserEncryption();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
// Group ciphers by orgId or under 'null' for the user's ciphers
const grouped = ciphers.reduce((agg, c) => {
@ -639,7 +639,7 @@ export class CipherService implements CipherServiceAbstraction {
): Promise<Cipher> {
let encKey: UserKey | OrgKey;
encKey = await this.cryptoService.getOrgKey(cipher.organizationId);
encKey ||= (await this.cryptoService.getKeyForUserEncryption()) as UserKey;
encKey ||= await this.cryptoService.getUserKeyWithLegacySupport();
const dataEncKey = await this.cryptoService.makeDataEncKey(encKey);
@ -956,7 +956,7 @@ export class CipherService implements CipherServiceAbstraction {
let encKey: UserKey | OrgKey;
encKey = await this.cryptoService.getOrgKey(organizationId);
encKey ||= (await this.cryptoService.getKeyForUserEncryption()) as UserKey;
encKey ||= (await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey;
const dataEncKey = await this.cryptoService.makeDataEncKey(encKey);