1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[Pm-13097] Rename cryptoservice to keyservice and move it to km ownership (#11358)

* Rename cryptoservice to keyservice

* Rename cryptoservice to keyservice

* Move key service to key management ownership

* Remove accidentally added file

* Fix cli build

* Fix browser build

* Run prettier

* Fix builds

* Fix cli build

* Fix tests

* Fix incorrect renames

* Rename webauthn-login-crypto-service

* Fix build errors due to merge conflicts

* Fix linting
This commit is contained in:
Bernd Schoolmann
2024-10-24 19:41:30 +02:00
committed by GitHub
parent 554171b688
commit b486fcc689
229 changed files with 1385 additions and 1446 deletions

View File

@@ -3,7 +3,7 @@ import { PrfKey } from "../../../types/key";
/**
* Contains methods for all crypto operations specific to the WebAuthn login flow.
*/
export abstract class WebAuthnLoginPrfCryptoServiceAbstraction {
export abstract class WebAuthnLoginPrfKeyServiceAbstraction {
/**
* Get the salt used to generate the PRF-output used when logging in with WebAuthn.
*/

View File

@@ -1,6 +1,7 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import {
FakeAccountService,
makeStaticByteArray,
@@ -8,7 +9,6 @@ import {
trackEmissions,
} from "../../../spec";
import { ApiService } from "../../abstractions/api.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
@@ -25,7 +25,7 @@ describe("AuthService", () => {
let accountService: FakeAccountService;
let messagingService: MockProxy<MessagingService>;
let cryptoService: MockProxy<CryptoService>;
let keyService: MockProxy<KeyService>;
let apiService: MockProxy<ApiService>;
let stateService: MockProxy<StateService>;
let tokenService: MockProxy<TokenService>;
@@ -36,7 +36,7 @@ describe("AuthService", () => {
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
messagingService = mock();
cryptoService = mock();
keyService = mock();
apiService = mock();
stateService = mock();
tokenService = mock();
@@ -44,7 +44,7 @@ describe("AuthService", () => {
sut = new AuthService(
accountService,
messagingService,
cryptoService,
keyService,
apiService,
stateService,
tokenService,
@@ -63,7 +63,7 @@ describe("AuthService", () => {
beforeEach(() => {
accountService.activeAccountSubject.next(accountInfo);
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
keyService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
});
it("emits LoggedOut when there is no active account", async () => {
@@ -84,7 +84,7 @@ describe("AuthService", () => {
it("emits LoggedOut when there is no access token but has a user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(false));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
keyService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(
AuthenticationStatus.LoggedOut,
@@ -93,14 +93,14 @@ describe("AuthService", () => {
it("emits Locked when there is an access token and no user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
keyService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Locked);
});
it("emits Unlocked when there is an access token and user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
keyService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Unlocked);
});
@@ -117,7 +117,7 @@ describe("AuthService", () => {
const emissions = trackEmissions(sut.activeAccountStatus$);
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
keyService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
accountService.activeAccountSubject.next(accountInfo2);
expect(emissions).toEqual([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked]);
@@ -150,7 +150,7 @@ describe("AuthService", () => {
describe("authStatusFor$", () => {
beforeEach(() => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
keyService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
});
it.each([null, undefined, "not a userId"])(
@@ -172,14 +172,14 @@ describe("AuthService", () => {
it("emits Locked when there is an access token and no user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
keyService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(AuthenticationStatus.Locked);
});
it("emits Unlocked when there is an access token and user key", async () => {
tokenService.hasAccessToken$.mockReturnValue(of(true));
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
keyService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(
AuthenticationStatus.Unlocked,

View File

@@ -9,8 +9,8 @@ import {
switchMap,
} from "rxjs";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { ApiService } from "../../abstractions/api.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service";
import { MessageSender } from "../../platform/messaging";
import { Utils } from "../../platform/misc/utils";
@@ -27,7 +27,7 @@ export class AuthService implements AuthServiceAbstraction {
constructor(
protected accountService: AccountService,
protected messageSender: MessageSender,
protected cryptoService: CryptoService,
protected keyService: KeyService,
protected apiService: ApiService,
protected stateService: StateService,
private tokenService: TokenService,
@@ -69,7 +69,7 @@ export class AuthService implements AuthServiceAbstraction {
}
return combineLatest([
this.cryptoService.getInMemoryUserKeyFor$(userId),
this.keyService.getInMemoryUserKeyFor$(userId),
this.tokenService.hasAccessToken$(userId),
]).pipe(
map(([userKey, hasAccessToken]) => {

View File

@@ -2,10 +2,10 @@ import { firstValueFrom, map, Observable } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
@@ -64,7 +64,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
constructor(
private keyGenerationService: KeyGenerationService,
private cryptoFunctionService: CryptoFunctionService,
private cryptoService: CryptoService,
private keyService: KeyService,
private encryptService: EncryptService,
private appIdService: AppIdService,
private devicesApiService: DevicesApiServiceAbstraction,
@@ -124,7 +124,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
}
// Attempt to get user key
const userKey: UserKey = await this.cryptoService.getUserKey(userId);
const userKey: UserKey = await this.keyService.getUserKey(userId);
// If user key is not found, throw error
if (!userKey) {
@@ -187,7 +187,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
}
// At this point of rotating their keys, they should still have their old user key in state
const oldUserKey = await firstValueFrom(this.cryptoService.userKey$(userId));
const oldUserKey = await firstValueFrom(this.keyService.userKey$(userId));
const deviceIdentifier = await this.appIdService.getAppId();
const secretVerificationRequest = new SecretVerificationRequest();

View File

@@ -4,6 +4,7 @@ import { BehaviorSubject, of } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
@@ -11,7 +12,6 @@ import { DeviceType } from "../../enums";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
@@ -43,7 +43,7 @@ describe("deviceTrustService", () => {
const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const appIdService = mock<AppIdService>();
const devicesApiService = mock<DevicesApiServiceAbstraction>();
@@ -368,7 +368,7 @@ describe("deviceTrustService", () => {
.mockResolvedValue(mockDeviceRsaKeyPair);
cryptoSvcGetUserKeySpy = jest
.spyOn(cryptoService, "getUserKey")
.spyOn(keyService, "getUserKey")
.mockResolvedValue(mockUserKey);
cryptoSvcRsaEncryptSpy = jest
@@ -623,7 +623,7 @@ describe("deviceTrustService", () => {
const fakeNewUserKeyData = new Uint8Array(64);
fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1);
fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey;
cryptoService.userKey$.mockReturnValue(of(fakeNewUserKey));
keyService.userKey$.mockReturnValue(of(fakeNewUserKey));
});
it("throws an error when a null user id is passed in", async () => {
@@ -659,7 +659,7 @@ describe("deviceTrustService", () => {
fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1);
// Mock the retrieval of a user key that differs from the new one passed into the method
cryptoService.userKey$.mockReturnValue(
keyService.userKey$.mockReturnValue(
of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey),
);
@@ -749,7 +749,7 @@ describe("deviceTrustService", () => {
return new DeviceTrustService(
keyGenerationService,
cryptoFunctionService,
cryptoService,
keyService,
encryptService,
appIdService,
devicesApiService,

View File

@@ -1,12 +1,12 @@
import { mock } from "jest-mock-extended";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import { ApiService } from "../../abstractions/api.service";
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationData } from "../../admin-console/models/data/organization.data";
import { Organization } from "../../admin-console/models/domain/organization";
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
@@ -27,7 +27,7 @@ import { TokenService } from "./token.service";
describe("KeyConnectorService", () => {
let keyConnectorService: KeyConnectorService;
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const apiService = mock<ApiService>();
const tokenService = mock<TokenService>();
const logService = mock<LogService>();
@@ -56,7 +56,7 @@ describe("KeyConnectorService", () => {
keyConnectorService = new KeyConnectorService(
accountService,
masterPasswordService,
cryptoService,
keyService,
apiService,
tokenService,
logService,

View File

@@ -2,12 +2,12 @@ import { firstValueFrom } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { ApiService } from "../../abstractions/api.service";
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserType } from "../../admin-console/enums";
import { Organization } from "../../admin-console/models/domain/organization";
import { KeysRequest } from "../../models/request/keys.request";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service";
import { KdfType } from "../../platform/enums/kdf-type.enum";
@@ -54,7 +54,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
constructor(
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private cryptoService: CryptoService,
private keyService: KeyService,
private apiService: ApiService,
private tokenService: TokenService,
private logService: LogService,
@@ -146,7 +146,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
? new PBKDF2KdfConfig(kdfIterations)
: new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const masterKey = await this.cryptoService.makeMasterKey(
const masterKey = await this.keyService.makeMasterKey(
password.keyB64,
await this.tokenService.getEmail(),
kdfConfig,
@@ -154,11 +154,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.cryptoService.makeUserKey(masterKey);
await this.cryptoService.setUserKey(userKey[0], userId);
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
const userKey = await this.keyService.makeUserKey(masterKey);
await this.keyService.setUserKey(userKey[0], userId);
await this.keyService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
const [pubKey, privKey] = await this.cryptoService.makeKeyPair(userKey[0]);
const [pubKey, privKey] = await this.keyService.makeKeyPair(userKey[0]);
try {
const keyConnectorUrl =

View File

@@ -5,9 +5,9 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { UserId } from "../../../../common/src/types/guid";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { AccountInfo, AccountService } from "../abstractions/account.service";
@@ -18,7 +18,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let accountService: MockProxy<AccountService>;
let cryptoService: MockProxy<CryptoService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let i18nService: MockProxy<I18nService>;
@@ -28,14 +28,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
organizationApiService = mock<OrganizationApiServiceAbstraction>();
accountService = mock<AccountService>();
accountService.activeAccount$ = activeAccountSubject;
cryptoService = mock<CryptoService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
organizationUserApiService = mock<OrganizationUserApiService>();
i18nService = mock<I18nService>();
service = new PasswordResetEnrollmentServiceImplementation(
organizationApiService,
accountService,
cryptoService,
keyService,
encryptService,
organizationUserApiService,
i18nService,
@@ -99,7 +99,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
};
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
keyService.getUserKey.mockResolvedValue({ key: "key" } as any);
encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
await service.enroll("orgId");

View File

@@ -6,8 +6,8 @@ import {
} from "@bitwarden/admin-console/common";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { Utils } from "../../platform/misc/utils";
import { UserKey } from "../../types/key";
@@ -20,7 +20,7 @@ export class PasswordResetEnrollmentServiceImplementation
constructor(
protected organizationApiService: OrganizationApiServiceAbstraction,
protected accountService: AccountService,
protected cryptoService: CryptoService,
protected keyService: KeyService,
protected encryptService: EncryptService,
protected organizationUserApiService: OrganizationUserApiService,
protected i18nService: I18nService,
@@ -47,7 +47,7 @@ export class PasswordResetEnrollmentServiceImplementation
userId =
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
userKey = userKey ?? (await this.keyService.getUserKey(userId));
// RSA Encrypt user's userKey.key with organization public key
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, orgPublicKey);

View File

@@ -8,9 +8,9 @@ import {
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
@@ -31,7 +31,7 @@ import { UserVerificationService } from "./user-verification.service";
describe("UserVerificationService", () => {
let sut: UserVerificationService;
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
const i18nService = mock<I18nService>();
const userVerificationApiService = mock<UserVerificationApiServiceAbstraction>();
@@ -50,7 +50,7 @@ describe("UserVerificationService", () => {
accountService = mockAccountServiceWith(mockUserId);
sut = new UserVerificationService(
cryptoService,
keyService,
accountService,
masterPasswordService,
i18nService,
@@ -132,7 +132,7 @@ describe("UserVerificationService", () => {
setMasterPasswordAvailability(false);
setPinAvailability("DISABLED");
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(isBiometricsLockSet);
cryptoService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored);
keyService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored);
platformUtilsService.supportsSecureStorage.mockReturnValue(platformSupportSecureStorage);
const result = await sut.getAvailableVerificationOptions("client");
@@ -205,7 +205,7 @@ describe("UserVerificationService", () => {
kdfConfigService.getKdfConfig.mockResolvedValue("kdfConfig" as unknown as KdfConfig);
masterPasswordService.masterKey$.mockReturnValue(of("masterKey" as unknown as MasterKey));
cryptoService.hashMasterKey
keyService.hashMasterKey
.calledWith("password", "masterKey" as unknown as MasterKey, HashPurpose.LocalAuthorization)
.mockResolvedValue("localHash");
});
@@ -216,7 +216,7 @@ describe("UserVerificationService", () => {
});
it("returns if verification is successful", async () => {
cryptoService.compareAndUpdateKeyHash.mockResolvedValueOnce(true);
keyService.compareAndUpdateKeyHash.mockResolvedValueOnce(true);
const result = await sut.verifyUserByMasterPassword(
{
@@ -227,7 +227,7 @@ describe("UserVerificationService", () => {
"email",
);
expect(cryptoService.compareAndUpdateKeyHash).toHaveBeenCalled();
expect(keyService.compareAndUpdateKeyHash).toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
"localHash",
mockUserId,
@@ -240,7 +240,7 @@ describe("UserVerificationService", () => {
});
it("throws if verification fails", async () => {
cryptoService.compareAndUpdateKeyHash.mockResolvedValueOnce(false);
keyService.compareAndUpdateKeyHash.mockResolvedValueOnce(false);
await expect(
sut.verifyUserByMasterPassword(
@@ -253,7 +253,7 @@ describe("UserVerificationService", () => {
),
).rejects.toThrow("Invalid master password");
expect(cryptoService.compareAndUpdateKeyHash).toHaveBeenCalled();
expect(keyService.compareAndUpdateKeyHash).toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith();
expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith();
});
@@ -265,7 +265,7 @@ describe("UserVerificationService", () => {
});
it("returns if verification is successful", async () => {
cryptoService.hashMasterKey
keyService.hashMasterKey
.calledWith(
"password",
"masterKey" as unknown as MasterKey,
@@ -285,7 +285,7 @@ describe("UserVerificationService", () => {
"email",
);
expect(cryptoService.compareAndUpdateKeyHash).not.toHaveBeenCalled();
expect(keyService.compareAndUpdateKeyHash).not.toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith(
"localHash",
mockUserId,
@@ -298,7 +298,7 @@ describe("UserVerificationService", () => {
});
it("throws if verification fails", async () => {
cryptoService.hashMasterKey
keyService.hashMasterKey
.calledWith(
"password",
"masterKey" as unknown as MasterKey,
@@ -318,7 +318,7 @@ describe("UserVerificationService", () => {
),
).rejects.toThrow("Invalid master password");
expect(cryptoService.compareAndUpdateKeyHash).not.toHaveBeenCalled();
expect(keyService.compareAndUpdateKeyHash).not.toHaveBeenCalled();
expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith();
expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith();
});
@@ -380,7 +380,7 @@ describe("UserVerificationService", () => {
it("throws if master key cannot be created", async () => {
kdfConfigService.getKdfConfig.mockResolvedValueOnce("kdfConfig" as unknown as KdfConfig);
masterPasswordService.masterKey$.mockReturnValueOnce(of(null));
cryptoService.makeMasterKey.mockResolvedValueOnce(null);
keyService.makeMasterKey.mockResolvedValueOnce(null);
await expect(
sut.verifyUserByMasterPassword(

View File

@@ -3,8 +3,8 @@ import { firstValueFrom, map } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
@@ -39,7 +39,7 @@ import {
*/
export class UserVerificationService implements UserVerificationServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private keyService: KeyService,
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private i18nService: I18nService,
@@ -66,7 +66,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
this.hasMasterPasswordAndMasterKeyHash(userId),
this.pinService.isPinDecryptionAvailable(userId),
this.vaultTimeoutSettingsService.isBiometricLockSet(userId),
this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric, userId),
this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric, userId),
]);
// note: we do not need to check this.platformUtilsService.supportsBiometric() because
@@ -119,7 +119,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
);
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (!masterKey && !alreadyHashed) {
masterKey = await this.cryptoService.makeMasterKey(
masterKey = await this.keyService.makeMasterKey(
verification.secret,
email,
await this.kdfConfigService.getKdfConfig(),
@@ -127,7 +127,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
}
request.masterPasswordHash = alreadyHashed
? verification.secret
: await this.cryptoService.hashMasterKey(verification.secret, masterKey);
: await this.keyService.hashMasterKey(verification.secret, masterKey);
}
return request;
@@ -196,7 +196,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (!masterKey) {
masterKey = await this.cryptoService.makeMasterKey(verification.secret, email, kdfConfig);
masterKey = await this.keyService.makeMasterKey(verification.secret, email, kdfConfig);
}
if (!masterKey) {
@@ -206,7 +206,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
let policyOptions: MasterPasswordPolicyResponse | null;
// Client-side verification
if (await this.hasMasterPasswordAndMasterKeyHash(userId)) {
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
const passwordValid = await this.keyService.compareAndUpdateKeyHash(
verification.secret,
masterKey,
);
@@ -217,7 +217,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
} else {
// Server-side verification
const request = new SecretVerificationRequest();
const serverKeyHash = await this.cryptoService.hashMasterKey(
const serverKeyHash = await this.keyService.hashMasterKey(
verification.secret,
masterKey,
HashPurpose.ServerAuthorization,
@@ -230,7 +230,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
}
}
const localKeyHash = await this.cryptoService.hashMasterKey(
const localKeyHash = await this.keyService.hashMasterKey(
verification.secret,
masterKey,
HashPurpose.LocalAuthorization,
@@ -254,7 +254,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
let userKey: UserKey;
// Biometrics crashes and doesn't return a value if the user cancels the prompt
try {
userKey = await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
userKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
} catch (e) {
this.logService.error(`Biometrics User Verification failed: ${e.message}`);
// So, any failures should be treated as a failed verification

View File

@@ -2,15 +2,15 @@ import { mock, MockProxy } from "jest-mock-extended";
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
import { WebAuthnLoginPrfCryptoService } from "./webauthn-login-prf-crypto.service";
import { WebAuthnLoginPrfKeyService } from "./webauthn-login-prf-key.service";
describe("WebAuthnLoginPrfCryptoService", () => {
describe("WebAuthnLoginPrfKeyService", () => {
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
let service: WebAuthnLoginPrfCryptoService;
let service: WebAuthnLoginPrfKeyService;
beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>();
service = new WebAuthnLoginPrfCryptoService(cryptoFunctionService);
service = new WebAuthnLoginPrfKeyService(cryptoFunctionService);
});
describe("createSymmetricKeyFromPrf", () => {

View File

@@ -1,11 +1,11 @@
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { PrfKey } from "../../../types/key";
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
import { WebAuthnLoginPrfKeyServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
const LoginWithPrfSalt = "passwordless-login";
export class WebAuthnLoginPrfCryptoService implements WebAuthnLoginPrfCryptoServiceAbstraction {
export class WebAuthnLoginPrfKeyService implements WebAuthnLoginPrfKeyServiceAbstraction {
constructor(private cryptoFunctionService: CryptoFunctionService) {}
async getLoginWithPrfSalt(): Promise<ArrayBuffer> {

View File

@@ -7,7 +7,7 @@ import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { PrfKey } from "../../../types/key";
import { WebAuthnLoginApiServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-api.service.abstraction";
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
import { WebAuthnLoginPrfKeyServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
import { AuthResult } from "../../models/domain/auth-result";
import { WebAuthnLoginCredentialAssertionOptionsView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
import { WebAuthnLoginCredentialAssertionView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion.view";
@@ -21,7 +21,7 @@ describe("WebAuthnLoginService", () => {
const webAuthnLoginApiService = mock<WebAuthnLoginApiServiceAbstraction>();
const loginStrategyService = mock<LoginStrategyServiceAbstraction>();
const webAuthnLoginPrfCryptoService = mock<WebAuthnLoginPrfCryptoServiceAbstraction>();
const webAuthnLoginPrfKeyService = mock<WebAuthnLoginPrfKeyServiceAbstraction>();
const navigatorCredentials = mock<CredentialsContainer>();
const logService = mock<LogService>();
@@ -72,7 +72,7 @@ describe("WebAuthnLoginService", () => {
return new WebAuthnLoginService(
webAuthnLoginApiService,
loginStrategyService,
webAuthnLoginPrfCryptoService,
webAuthnLoginPrfKeyService,
window,
logService,
);
@@ -141,8 +141,8 @@ describe("WebAuthnLoginService", () => {
publicKeyCredential.getClientExtensionResults().prf?.results?.first;
const prfKey = new SymmetricCryptoKey(new Uint8Array(prfResult)) as PrfKey;
webAuthnLoginPrfCryptoService.getLoginWithPrfSalt.mockResolvedValue(saltArrayBuffer);
webAuthnLoginPrfCryptoService.createSymmetricKeyFromPrf.mockResolvedValue(prfKey);
webAuthnLoginPrfKeyService.getLoginWithPrfSalt.mockResolvedValue(saltArrayBuffer);
webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf.mockResolvedValue(prfKey);
// Mock implementations
navigatorCredentials.get.mockResolvedValue(publicKeyCredential);
@@ -152,7 +152,7 @@ describe("WebAuthnLoginService", () => {
// Assert
expect(webAuthnLoginPrfCryptoService.getLoginWithPrfSalt).toHaveBeenCalled();
expect(webAuthnLoginPrfKeyService.getLoginWithPrfSalt).toHaveBeenCalled();
expect(navigatorCredentials.get).toHaveBeenCalledWith(
expect.objectContaining({
@@ -169,9 +169,7 @@ describe("WebAuthnLoginService", () => {
}),
);
expect(webAuthnLoginPrfCryptoService.createSymmetricKeyFromPrf).toHaveBeenCalledWith(
prfResult,
);
expect(webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf).toHaveBeenCalledWith(prfResult);
expect(result).toBeInstanceOf(WebAuthnLoginCredentialAssertionView);
expect(result.token).toEqual(credentialAssertionOptions.token);

View File

@@ -3,7 +3,7 @@ import { LoginStrategyServiceAbstraction, WebAuthnLoginCredentials } from "@bitw
import { LogService } from "../../../platform/abstractions/log.service";
import { PrfKey } from "../../../types/key";
import { WebAuthnLoginApiServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-api.service.abstraction";
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
import { WebAuthnLoginPrfKeyServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "../../abstractions/webauthn/webauthn-login.service.abstraction";
import { AuthResult } from "../../models/domain/auth-result";
import { WebAuthnLoginCredentialAssertionOptionsView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
@@ -17,7 +17,7 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction {
constructor(
private webAuthnLoginApiService: WebAuthnLoginApiServiceAbstraction,
private loginStrategyService: LoginStrategyServiceAbstraction,
private webAuthnLoginPrfCryptoService: WebAuthnLoginPrfCryptoServiceAbstraction,
private webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction,
private window: Window,
private logService?: LogService,
) {
@@ -37,7 +37,7 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction {
};
// TODO: Remove `any` when typescript typings add support for PRF
nativeOptions.publicKey.extensions = {
prf: { eval: { first: await this.webAuthnLoginPrfCryptoService.getLoginWithPrfSalt() } },
prf: { eval: { first: await this.webAuthnLoginPrfKeyService.getLoginWithPrfSalt() } },
} as any;
try {
@@ -50,7 +50,7 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction {
let symmetricPrfKey: PrfKey | undefined;
if (prfResult != undefined) {
symmetricPrfKey =
await this.webAuthnLoginPrfCryptoService.createSymmetricKeyFromPrf(prfResult);
await this.webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf(prfResult);
}
const deviceResponse = new WebAuthnLoginAssertionResponseRequest(response);

View File

@@ -1,9 +1,9 @@
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { ApiService } from "../../abstractions/api.service";
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { EncString } from "../../platform/models/domain/enc-string";
@@ -28,7 +28,7 @@ interface OrganizationKeys {
export class OrganizationBillingService implements OrganizationBillingServiceAbstraction {
constructor(
private apiService: ApiService,
private cryptoService: CryptoService,
private keyService: KeyService,
private encryptService: EncryptService,
private i18nService: I18nService,
private organizationApiService: OrganizationApiService,
@@ -78,8 +78,8 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
}
private async makeOrganizationKeys(): Promise<OrganizationKeys> {
const [encryptedKey, key] = await this.cryptoService.makeOrgKey<OrgKey>();
const [publicKey, encryptedPrivateKey] = await this.cryptoService.makeKeyPair(key);
const [encryptedKey, key] = await this.keyService.makeOrgKey<OrgKey>();
const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key);
const encryptedCollectionName = await this.encryptService.encrypt(
this.i18nService.t("defaultCollection"),
key,

View File

@@ -1,424 +0,0 @@
import { Observable } from "rxjs";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { OrganizationId, UserId } from "../../types/guid";
import {
UserKey,
MasterKey,
OrgKey,
ProviderKey,
CipherKey,
UserPrivateKey,
UserPublicKey,
} from "../../types/key";
import { KeySuffixOptions, HashPurpose } from "../enums";
import { EncryptedString, EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export class UserPrivateKeyDecryptionFailedError extends Error {
constructor() {
super("Failed to decrypt the user's private key.");
}
}
/**
* An object containing all the users key needed to decrypt a users personal and organization vaults.
*/
export type CipherDecryptionKeys = {
/**
* A users {@link UserKey} that is useful for decrypted ciphers in the users personal vault.
*/
userKey: UserKey;
/**
* A users decrypted organization keys.
*/
orgKeys: Record<OrganizationId, OrgKey>;
};
export abstract class CryptoService {
/**
* Retrieves a stream of the given users {@see UserKey} values. Can emit null if the user does not have a user key, e.g. the user
* is in a locked or logged out state.
* @param userId The user id of the user to get the {@see UserKey} for.
*/
abstract userKey$(userId: UserId): Observable<UserKey>;
/**
* Returns the an observable key for the given user id.
*
* @note this observable represents only user keys stored in memory. A null value does not indicate that we cannot load a user key from storage.
* @param userId The desired user
*/
abstract getInMemoryUserKeyFor$(userId: UserId): Observable<UserKey>;
/**
* Sets the provided user key and stores
* any other necessary versions (such as auto, biometrics,
* or pin)
*
* @throws when key is null. Lock the account to clear a key
* @param key The user key to set
* @param userId The desired user
*/
abstract setUserKey(key: UserKey, userId?: string): Promise<void>;
/**
* Sets the provided user keys and stores any other necessary versions
* (such as auto, biometrics, or pin).
* Also sets the user's encrypted private key in storage and
* clears the decrypted private key from memory
* Note: does not clear the private key if null is provided
*
* @throws Error when userKey, encPrivateKey or userId is null
* @throws UserPrivateKeyDecryptionFailedError when the userKey cannot decrypt encPrivateKey
* @param userKey The user key to set
* @param encPrivateKey An encrypted private key
* @param userId The desired user
*/
abstract setUserKeys(userKey: UserKey, encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Gets the user key from memory and sets it again,
* kicking off a refresh of any additional keys
* (such as auto, biometrics, or pin)
*/
abstract refreshAdditionalKeys(): Promise<void>;
/**
* Observable value that returns whether or not the currently active user has ever had auser key,
* i.e. has ever been unlocked/decrypted. This is key for differentiating between TDE locked and standard locked states.
*/
abstract everHadUserKey$: Observable<boolean>;
/**
* Retrieves the user key
* @param userId The desired user
* @returns The user key
*
* @deprecated Use {@link userKey$} with a required {@link UserId} instead.
*/
abstract getUserKey(userId?: string): Promise<UserKey>;
/**
* Checks if the user is using an old encryption scheme that used the master key
* for encryption of data instead of the user key.
*/
abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise<boolean>;
/**
* 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.
*
* @deprecated Please provide the userId of the user you want the user key for.
*/
abstract getUserKeyWithLegacySupport(): 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
*/
abstract getUserKeyWithLegacySupport(userId: UserId): 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
*/
abstract getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise<UserKey>;
/**
* Determines whether the user key is available for the given user.
* @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false.
* @returns True if the user key is available
*/
abstract hasUserKey(userId?: UserId): Promise<boolean>;
/**
* Determines whether the user key is available for the given user in memory.
* @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false.
* @returns True if the user key is available
*/
abstract hasUserKeyInMemory(userId?: string): Promise<boolean>;
/**
* @param keySuffix The desired version of the user's key to check
* @param userId The desired user
* @returns True if the provided version of the user key is stored
*/
abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean>;
/**
* Generates a new user key
* @param masterKey The user's master key
* @returns A new user key and the master key protected version of it
*/
abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>;
/**
* Clears the user's stored version of the user key
* @param keySuffix The desired version of the key to clear
* @param userId The desired user
*/
abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
/**
* Stores the master key encrypted user key
* @param userKeyMasterKey The master key encrypted user key to set
* @param userId The desired user
*/
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId: string): Promise<void>;
/**
* @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
*/
abstract getOrDeriveMasterKey(password: string, userId?: string): Promise<MasterKey>;
/**
* Generates a master key from the provided password
* @param password The user's master password
* @param email The user's email
* @param KdfConfig The user's key derivation function configuration
* @returns A master key derived from the provided password
*/
abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey>;
/**
* Encrypts the existing (or provided) user key with the
* provided master key
* @param masterKey The user's master key
* @param userKey The user key
* @returns The user key and the master key protected version of it
*/
abstract encryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: UserKey,
): Promise<[UserKey, EncString]>;
/**
* Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending
* on the hashPurpose provided.
* @param password The user's master password
* @param key The user's master key
* @param hashPurpose The iterations to use for the hash
* @returns The user's master password hash
*/
abstract hashMasterKey(
password: string,
key: MasterKey,
hashPurpose?: HashPurpose,
): Promise<string>;
/**
* Compares the provided master password to the stored password hash and server password hash.
* Updates the stored hash if outdated.
* @param masterPassword The user's master password
* @param key The user's master key
* @returns True if the provided master password matches either the stored
* key hash or the server key hash
*/
abstract compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean>;
/**
* Stores the encrypted organization keys and clears any decrypted
* organization keys currently in memory
* @param orgs The organizations to set keys for
* @param providerOrgs The provider organizations to set keys for
* @param userId The user id of the user to set the org keys for
*/
abstract setOrgKeys(
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
userId: UserId,
): Promise<void>;
/**
* Retrieves a stream of the active users organization keys,
* will NOT emit any value if there is no active user.
*
* @deprecated Use {@link orgKeys$} with a required {@link UserId} instead.
*/
abstract activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
/**
* Returns the organization's symmetric key
* @deprecated Use the observable userOrgKeys$ and `map` to the desired {@link OrgKey} instead
* @param orgId The desired organization
* @returns The organization's symmetric key
*/
abstract getOrgKey(orgId: string): Promise<OrgKey>;
/**
* Uses the org key to derive a new symmetric key for encrypting data
* @param orgKey The organization's symmetric key
*/
abstract makeDataEncKey<T extends UserKey | OrgKey>(
key: T,
): Promise<[SymmetricCryptoKey, EncString]>;
/**
* Stores the provider keys for a given user.
* @param orgs The provider orgs for which to save the keys from.
* @param userId The user id of the user for which to store the keys for.
*/
abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>;
/**
* @param providerId The desired provider
* @returns The provider's symmetric key
*/
abstract getProviderKey(providerId: string): Promise<ProviderKey>;
/**
* 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.
* @returns The new encrypted org key and the decrypted key itself
*/
abstract makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]>;
/**
* Sets the user's encrypted private key in storage and
* clears the decrypted private key from memory
* Note: does not clear the private key if null is provided
* @param encPrivateKey An encrypted private key
*/
abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Returns the private key from memory. If not available, decrypts it
* from storage and stores it in memory
* @returns The user's private key
*
* @throws An error if there is no user currently active.
*
* @deprecated Use {@link userPrivateKey$} instead.
*/
abstract getPrivateKey(): Promise<Uint8Array>;
/**
* Gets an observable stream of the given users decrypted private key, will emit null if the user
* doesn't have a UserKey to decrypt the encrypted private key or null if the user doesn't have an
* encrypted private key at all.
*
* @param userId The user id of the user to get the data for.
*/
abstract userPrivateKey$(userId: UserId): Observable<UserPrivateKey>;
/**
* Gets an observable stream of the given users encrypted private key, will emit null if the user
* doesn't have an encrypted private key at all.
*
* @param userId The user id of the user to get the data for.
*
* @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.
*/
abstract userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString>;
/**
* Gets an observable stream of the given users decrypted private key with legacy support,
* will emit null if the user doesn't have a UserKey to decrypt the encrypted private key
* or null if the user doesn't have an encrypted private key at all.
*
* @param userId The user id of the user to get the data for.
*/
abstract userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey>;
/**
* Generates a fingerprint phrase for the user based on their public key
* @param fingerprintMaterial Fingerprint material
* @param publicKey The user's public key
* @returns The user's fingerprint phrase
*/
abstract getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]>;
/**
* Generates a new keypair
* @param key A key to encrypt the private key with. If not provided,
* defaults to the user key
* @returns A new keypair: [publicKey in Base64, encrypted privateKey]
* @throws If the provided key is a null-ish value.
*/
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>;
/**
* 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
*/
abstract clearPinKeys(userId?: string): Promise<void>;
/**
* @param keyMaterial The key material to derive the send key from
* @returns A new send key
*/
abstract makeSendKey(keyMaterial: Uint8Array): Promise<SymmetricCryptoKey>;
/**
* Clears all of the user's keys from storage
* @param userId The user's Id
*/
abstract clearKeys(userId?: string): Promise<any>;
abstract randomNumber(min: number, max: number): Promise<number>;
/**
* Generates a new cipher key
* @returns A new cipher key
*/
abstract makeCipherKey(): Promise<CipherKey>;
/**
* Initialize all necessary crypto keys needed for a new account.
* Warning! This completely replaces any existing keys!
* @returns The user's newly created public key, private key, and encrypted private key
*
* @throws An error if there is no user currently active.
*/
abstract initAccount(): Promise<{
userKey: UserKey;
publicKey: string;
privateKey: EncString;
}>;
/**
* 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
*/
abstract clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string): Promise<void>;
/**
* Retrieves all the keys needed for decrypting Ciphers
* @param userId The user id of the keys to retrieve or null if the user is not Unlocked
* @param legacySupport `true` if you need to support retrieving the legacy version of the users key, `false` if
* you do not need legacy support. Use `true` by necessity only. Defaults to `false`. Legacy support is for users
* that may not have updated to use the new {@link UserKey} yet.
*
* @throws If an invalid user id is passed in.
*/
abstract cipherDecryptionKeys$(
userId: UserId,
legacySupport?: boolean,
): Observable<CipherDecryptionKeys | null>;
/**
* Gets an observable of org keys for the given user.
* @param userId The user id of the user of which to get the keys for.
* @return An observable stream of the users organization keys if they are unlocked, or null if the user is not unlocked.
* The observable will stay alive through locks/unlocks.
*
* @throws If an invalid user id is passed in.
*/
abstract orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null>;
/**
* Gets an observable stream of the given users encrypted organisation keys.
*
* @param userId The user id of the user to get the data for.
*
* @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.
*/
abstract encryptedOrgKeys$(
userId: UserId,
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>>;
/**
* Gets an observable stream of the users public key. If the user is does not have
* a {@link UserKey} or {@link UserPrivateKey} that is decryptable, this will emit null.
*
* @param userId The user id of the user of which to get the public key for.
*
* @throws If an invalid user id is passed in.
*/
abstract userPublicKey$(userId: UserId): Observable<UserPublicKey>;
/**
* Validates that a userkey is correct for a given user
* @param key The key to validate
* @param userId The user id for the key
*/
abstract validateUserKey(key: UserKey, userId: UserId): Promise<boolean>;
}

View File

@@ -6,7 +6,7 @@ import { Observable, of, switchMap } from "rxjs";
import { getHostname, parse } from "tldts";
import { Merge } from "type-fest";
import { CryptoService } from "../abstractions/crypto.service";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { I18nService } from "../abstractions/i18n.service";
@@ -18,7 +18,7 @@ declare global {
}
interface BitwardenContainerService {
getCryptoService: () => CryptoService;
getKeyService: () => KeyService;
getEncryptService: () => EncryptService;
}

View File

@@ -1,10 +1,10 @@
import { mock, MockProxy } from "jest-mock-extended";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { makeEncString, makeStaticByteArray } from "../../../../spec";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { UserKey, OrgKey } from "../../../types/key";
import { CryptoService } from "../../abstractions/crypto.service";
import { EncryptionType } from "../../enums";
import { Utils } from "../../misc/utils";
import { ContainerService } from "../../services/container.service";
@@ -81,9 +81,9 @@ describe("EncString", () => {
describe("decrypt", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
const cryptoService = mock<CryptoService>();
cryptoService.hasUserKey.mockResolvedValue(true);
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(
const keyService = mock<KeyService>();
keyService.hasUserKey.mockResolvedValue(true);
keyService.getUserKeyWithLegacySupport.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(32)) as UserKey,
);
@@ -94,7 +94,7 @@ describe("EncString", () => {
beforeEach(() => {
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
keyService,
encryptService,
);
});
@@ -117,7 +117,7 @@ describe("EncString", () => {
describe("decryptWithKey", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
encryptService.decryptToUtf8
.calledWith(encString, expect.anything())
@@ -140,10 +140,7 @@ describe("EncString", () => {
}
beforeEach(() => {
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService,
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
});
it("decrypts using the provided key and encryptService", async () => {
@@ -321,28 +318,22 @@ describe("EncString", () => {
});
describe("decrypt", () => {
let cryptoService: MockProxy<CryptoService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
let encString: EncString;
beforeEach(() => {
cryptoService = mock<CryptoService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
encString = new EncString(null);
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService,
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
});
it("handles value it can't decrypt", async () => {
encryptService.decryptToUtf8.mockRejectedValue("error");
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService,
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
const decrypted = await encString.decrypt(null);
@@ -354,34 +345,34 @@ describe("EncString", () => {
});
});
it("uses provided key without depending on CryptoService", async () => {
it("uses provided key without depending on KeyService", async () => {
const key = mock<SymmetricCryptoKey>();
await encString.decrypt(null, key);
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
});
it("gets an organization key if required", async () => {
const orgKey = mock<OrgKey>();
cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
await encString.decrypt("orgId", null);
expect(cryptoService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, orgKey);
});
it("gets the user's decryption key if required", async () => {
const userKey = mock<UserKey>();
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
await encString.decrypt(null, null);
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, userKey);
});
});

View File

@@ -189,10 +189,10 @@ export class EncString implements Encrypted {
return this.decryptedValue;
}
private async getKeyForDecryption(orgId: string) {
const cryptoService = Utils.getContainerService().getCryptoService();
const keyService = Utils.getContainerService().getKeyService();
return orgId != null
? await cryptoService.getOrgKey(orgId)
: await cryptoService.getUserKeyWithLegacySupport();
? await keyService.getOrgKey(orgId)
: await keyService.getUserKeyWithLegacySupport();
}
}

View File

@@ -1,9 +1,9 @@
import { CryptoService } from "../abstractions/crypto.service";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { EncryptService } from "../abstractions/encrypt.service";
export class ContainerService {
constructor(
private cryptoService: CryptoService,
private keyService: KeyService,
private encryptService: EncryptService,
) {}
@@ -14,13 +14,13 @@ export class ContainerService {
}
/**
* @throws Will throw if CryptoService was not instantiated and provided to the ContainerService constructor
* @throws Will throw if KeyService was not instantiated and provided to the ContainerService constructor
*/
getCryptoService(): CryptoService {
if (this.cryptoService == null) {
throw new Error("ContainerService.cryptoService not initialized.");
getKeyService(): KeyService {
if (this.keyService == null) {
throw new Error("ContainerService.keyService not initialized.");
}
return this.cryptoService;
return this.keyService;
}
/**

View File

@@ -1,738 +0,0 @@
import { mock } from "jest-mock-extended";
import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import {
awaitAsync,
makeEncString,
makeStaticByteArray,
makeSymmetricCryptoKey,
} from "../../../spec";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state";
import { CsprngArray } from "../../types/csprng";
import { OrganizationId, UserId } from "../../types/guid";
import { UserKey, MasterKey } from "../../types/key";
import { VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { UserPrivateKeyDecryptionFailedError } from "../abstractions/crypto.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { KeyGenerationService } from "../abstractions/key-generation.service";
import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import { Encrypted } from "../interfaces/encrypted";
import { Utils } from "../misc/utils";
import { EncString, EncryptedString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { CryptoService } from "../services/crypto.service";
import { UserKeyDefinition } from "../state";
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state";
import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state";
import {
USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY,
USER_KEY,
} from "./key-state/user-key.state";
describe("cryptoService", () => {
let cryptoService: CryptoService;
const pinService = mock<PinServiceAbstraction>();
const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
const platformUtilService = mock<PlatformUtilsService>();
const logService = mock<LogService>();
const stateService = mock<StateService>();
const kdfConfigService = mock<KdfConfigService>();
let stateProvider: FakeStateProvider;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let masterPasswordService: FakeMasterPasswordService;
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
masterPasswordService = new FakeMasterPasswordService();
stateProvider = new FakeStateProvider(accountService);
cryptoService = new CryptoService(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,
encryptService,
platformUtilService,
logService,
stateService,
accountService,
stateProvider,
kdfConfigService,
);
});
afterEach(() => {
jest.resetAllMocks();
});
it("instantiates", () => {
expect(cryptoService).not.toBeFalsy();
});
describe("getUserKey", () => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
it("retrieves the key state of the requested user", async () => {
await cryptoService.getUserKey(mockUserId);
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(USER_KEY, mockUserId);
});
it("returns the User Key if available", async () => {
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
const userKey = await cryptoService.getUserKey(mockUserId);
expect(userKey).toEqual(mockUserKey);
});
it("returns nullish if the user key is not set", async () => {
const userKey = await cryptoService.getUserKey(mockUserId);
expect(userKey).toBeFalsy();
});
});
describe.each(["hasUserKey", "hasUserKeyInMemory"])(
`%s`,
(method: "hasUserKey" | "hasUserKeyInMemory") => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
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 cryptoService[method](mockUserId)).toBe(hasKey);
});
it("returns false when no active userId is set", async () => {
accountService.activeAccountSubject.next(null);
expect(await cryptoService[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 cryptoService[method]()).toBe(hasKey);
},
);
},
);
describe("getUserKeyWithLegacySupport", () => {
let mockUserKey: UserKey;
let mockMasterKey: MasterKey;
let getMasterKey: jest.SpyInstance;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
getMasterKey = jest.spyOn(masterPasswordService, "masterKey$");
});
it("returns the User Key if available", async () => {
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
const getKeySpy = jest.spyOn(cryptoService, "getUserKey");
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(getKeySpy).toHaveBeenCalledWith(mockUserId);
expect(getMasterKey).not.toHaveBeenCalled();
expect(userKey).toEqual(mockUserKey);
});
it("returns the user's master key when User Key is not available", async () => {
masterPasswordService.masterKeySubject.next(mockMasterKey);
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(getMasterKey).toHaveBeenCalledWith(mockUserId);
expect(userKey).toEqual(mockMasterKey);
});
});
describe("everHadUserKey$", () => {
let everHadUserKeyState: FakeActiveUserState<boolean>;
beforeEach(() => {
everHadUserKeyState = stateProvider.activeUser.getFake(USER_EVER_HAD_USER_KEY);
});
it("should return true when stored value is true", async () => {
everHadUserKeyState.nextState(true);
expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(true);
});
it("should return false when stored value is false", async () => {
everHadUserKeyState.nextState(false);
expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(false);
});
it("should return false when stored value is null", async () => {
everHadUserKeyState.nextState(null);
expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(false);
});
});
describe("setUserKey", () => {
let mockUserKey: UserKey;
let everHadUserKeyState: FakeSingleUserState<boolean>;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
everHadUserKeyState = stateProvider.singleUser.getFake(mockUserId, USER_EVER_HAD_USER_KEY);
// Initialize storage
everHadUserKeyState.nextState(null);
});
it("should set everHadUserKey if key is not null to true", async () => {
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(await firstValueFrom(everHadUserKeyState.state$)).toBe(true);
});
describe("Auto Key refresh", () => {
it("sets an Auto key if vault timeout is set to 'never'", async () => {
await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(mockUserKey.keyB64, {
userId: mockUserId,
});
});
it("clears the Auto key if vault timeout is set to anything other than null", async () => {
await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
it("clears the old deprecated Auto key whenever a User Key is set", async () => {
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setCryptoMasterKeyAuto).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
});
it("throws if key is null", async () => {
await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided.");
});
it("throws if userId is null", async () => {
await expect(cryptoService.setUserKey(mockUserKey, null)).rejects.toThrow(
"No userId provided.",
);
});
describe("Pin Key refresh", () => {
const mockPinKeyEncryptedUserKey = new EncString(
"2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
);
const mockUserKeyEncryptedPin = new EncString(
"2.BBBw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
);
it("sets a pinKeyEncryptedUserKeyPersistent if a userKeyEncryptedPin and pinKeyEncryptedUserKey is set", async () => {
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
mockPinKeyEncryptedUserKey,
);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
mockPinKeyEncryptedUserKey,
false,
mockUserId,
);
});
it("sets a pinKeyEncryptedUserKeyEphemeral if a userKeyEncryptedPin is set, but a pinKeyEncryptedUserKey is not set", async () => {
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
mockPinKeyEncryptedUserKey,
true,
mockUserId,
);
});
it("clears the pinKeyEncryptedUserKeyPersistent and pinKeyEncryptedUserKeyEphemeral if the UserKeyEncryptedPin is not set", async () => {
pinService.getUserKeyEncryptedPin.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(pinService.clearPinKeyEncryptedUserKeyPersistent).toHaveBeenCalledWith(mockUserId);
expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(mockUserId);
});
});
});
describe("setUserKeys", () => {
let mockUserKey: UserKey;
let mockEncPrivateKey: EncryptedString;
let everHadUserKeyState: FakeSingleUserState<boolean>;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
mockEncPrivateKey = new SymmetricCryptoKey(mockRandomBytes).toString() as EncryptedString;
everHadUserKeyState = stateProvider.singleUser.getFake(mockUserId, USER_EVER_HAD_USER_KEY);
// Initialize storage
everHadUserKeyState.nextState(null);
// Mock private key decryption
encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes);
});
it("throws if userKey is null", async () => {
await expect(cryptoService.setUserKeys(null, mockEncPrivateKey, mockUserId)).rejects.toThrow(
"No userKey provided.",
);
});
it("throws if encPrivateKey is null", async () => {
await expect(cryptoService.setUserKeys(mockUserKey, null, mockUserId)).rejects.toThrow(
"No encPrivateKey provided.",
);
});
it("throws if userId is null", async () => {
await expect(cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, null)).rejects.toThrow(
"No userId provided.",
);
});
it("throws if encPrivateKey cannot be decrypted with the userKey", async () => {
encryptService.decryptToBytes.mockResolvedValue(null);
await expect(
cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId),
).rejects.toThrow(UserPrivateKeyDecryptionFailedError);
});
// We already have tests for setUserKey, so we just need to test that the correct methods are called
it("calls setUserKey with the userKey and userId", async () => {
const setUserKeySpy = jest.spyOn(cryptoService, "setUserKey");
await cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId);
expect(setUserKeySpy).toHaveBeenCalledWith(mockUserKey, mockUserId);
});
// We already have tests for setPrivateKey, so we just need to test that the correct methods are called
// TODO: Move those tests into here since `setPrivateKey` will be converted to a private method
it("calls setPrivateKey with the encPrivateKey and userId", async () => {
const setEncryptedPrivateKeySpy = jest.spyOn(cryptoService, "setPrivateKey");
await cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId);
expect(setEncryptedPrivateKeySpy).toHaveBeenCalledWith(mockEncPrivateKey, mockUserId);
});
});
describe("clearKeys", () => {
it("resolves active user id when called with no user id", async () => {
let callCount = 0;
stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++));
await cryptoService.clearKeys(null);
expect(callCount).toBe(1);
// revert to the original state
accountService.activeAccount$ = accountService.activeAccountSubject.asObservable();
});
describe.each([
USER_ENCRYPTED_ORGANIZATION_KEYS,
USER_ENCRYPTED_PROVIDER_KEYS,
USER_ENCRYPTED_PRIVATE_KEY,
USER_KEY,
])("key removal", (key: UserKeyDefinition<unknown>) => {
it(`clears ${key.key} for active user when unspecified`, async () => {
await cryptoService.clearKeys(null);
const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null);
});
it(`clears ${key.key} for the specified user when specified`, async () => {
const userId = "someOtherUser" as UserId;
await cryptoService.clearKeys(userId);
const encryptedOrgKeyState = stateProvider.singleUser.getFake(userId, key);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null);
});
});
});
describe("userPrivateKey$", () => {
type SetupKeysParams = {
makeMasterKey: boolean;
makeUserKey: boolean;
};
function setupKeys({ makeMasterKey, makeUserKey }: SetupKeysParams): [UserKey, MasterKey] {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null;
masterPasswordService.masterKeySubject.next(fakeMasterKey);
userKeyState.nextState(null);
const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey<UserKey>(64) : null;
userKeyState.nextState(fakeUserKey);
return [fakeUserKey, fakeMasterKey];
}
it("will return users decrypted private key when user has a user key and encrypted private key set", async () => {
const [userKey] = setupKeys({
makeMasterKey: false,
makeUserKey: true,
});
const userEncryptedPrivateKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
const fakeEncryptedUserPrivateKey = makeEncString("1");
userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString);
// Decryption of the user private key
const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1);
encryptService.decryptToBytes.mockResolvedValue(fakeDecryptedUserPrivateKey);
const fakeUserPublicKey = makeStaticByteArray(10, 2);
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(fakeUserPublicKey);
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(
fakeEncryptedUserPrivateKey,
userKey,
);
expect(userPrivateKey).toBe(fakeDecryptedUserPrivateKey);
});
it("returns null user private key when no user key is found", async () => {
setupKeys({ makeMasterKey: false, makeUserKey: false });
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
expect(encryptService.decryptToBytes).not.toHaveBeenCalled();
expect(userPrivateKey).toBeFalsy();
});
it("returns null when user does not have a private key set", async () => {
setupKeys({ makeUserKey: true, makeMasterKey: false });
const encryptedUserPrivateKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
encryptedUserPrivateKeyState.nextState(null);
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
expect(userPrivateKey).toBeFalsy();
});
});
describe("cipherDecryptionKeys$", () => {
function fakePrivateKeyDecryption(encryptedPrivateKey: Encrypted, key: SymmetricCryptoKey) {
const output = new Uint8Array(64);
output.set(encryptedPrivateKey.dataBytes);
output.set(
key.key.subarray(0, 64 - encryptedPrivateKey.dataBytes.length),
encryptedPrivateKey.dataBytes.length,
);
return output;
}
function fakeOrgKeyDecryption(encryptedString: EncString, userPrivateKey: Uint8Array) {
const output = new Uint8Array(64);
output.set(encryptedString.dataBytes);
output.set(
userPrivateKey.subarray(0, 64 - encryptedString.dataBytes.length),
encryptedString.dataBytes.length,
);
return output;
}
const org1Id = "org1" as OrganizationId;
type UpdateKeysParams = {
userKey: UserKey;
encryptedPrivateKey: EncString;
orgKeys: Record<string, EncryptedOrganizationKeyData>;
providerKeys: Record<string, EncryptedString>;
};
function updateKeys(keys: Partial<UpdateKeysParams> = {}) {
if ("userKey" in keys) {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
userKeyState.nextState(keys.userKey);
}
if ("encryptedPrivateKey" in keys) {
const userEncryptedPrivateKey = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey.encryptedString);
}
if ("orgKeys" in keys) {
const orgKeysState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_ORGANIZATION_KEYS,
);
orgKeysState.nextState(keys.orgKeys);
}
if ("providerKeys" in keys) {
const providerKeysState = stateProvider.singleUser.getFake(
mockUserId,
USER_ENCRYPTED_PROVIDER_KEYS,
);
providerKeysState.nextState(keys.providerKeys);
}
encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => {
// TOOD: Branch between provider and private key?
return Promise.resolve(fakePrivateKeyDecryption(encryptedPrivateKey, userKey));
});
encryptService.rsaDecrypt.mockImplementation((data, privateKey) => {
return Promise.resolve(fakeOrgKeyDecryption(data, privateKey));
});
}
it("returns decryption keys when there are no org or provider keys set", async () => {
updateKeys({
userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
});
const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).toEqual({});
});
it("returns decryption keys when there are org keys", async () => {
updateKeys({
userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
},
});
const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(1);
expect(decryptionKeys.orgKeys[org1Id]).not.toBeNull();
const orgKey = decryptionKeys.orgKeys[org1Id];
expect(orgKey.keyB64).toContain("org1Key");
});
it("returns decryption keys when there is an empty record for provider keys", async () => {
updateKeys({
userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
},
providerKeys: {},
});
const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(1);
expect(decryptionKeys.orgKeys[org1Id]).not.toBeNull();
const orgKey = decryptionKeys.orgKeys[org1Id];
expect(orgKey.keyB64).toContain("org1Key");
});
it("returns decryption keys when some of the org keys are providers", async () => {
const org2Id = "org2Id" as OrganizationId;
updateKeys({
userKey: makeSymmetricCryptoKey<UserKey>(64),
encryptedPrivateKey: makeEncString("privateKey"),
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
[org2Id]: {
type: "provider",
key: makeEncString("provider1Key").encryptedString,
providerId: "provider1",
},
},
providerKeys: {
provider1: makeEncString("provider1Key").encryptedString,
},
});
const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId));
expect(decryptionKeys).not.toBeNull();
expect(decryptionKeys.userKey).not.toBeNull();
expect(decryptionKeys.orgKeys).not.toBeNull();
expect(Object.keys(decryptionKeys.orgKeys)).toHaveLength(2);
const orgKey = decryptionKeys.orgKeys[org1Id];
expect(orgKey).not.toBeNull();
expect(orgKey.keyB64).toContain("org1Key");
const org2Key = decryptionKeys.orgKeys[org2Id];
expect(org2Key).not.toBeNull();
expect(org2Key.keyB64).toContain("provider1Key");
});
it("returns a stream that pays attention to updates of all data", async () => {
// Start listening until there have been 6 emissions
const promise = lastValueFrom(
cryptoService.cipherDecryptionKeys$(mockUserId).pipe(bufferCount(6), take(1)),
);
// User has their UserKey set
const initialUserKey = makeSymmetricCryptoKey<UserKey>(64);
updateKeys({
userKey: initialUserKey,
});
// Because switchMap is a little to good at its job
await awaitAsync();
// User has their private key set
const initialPrivateKey = makeEncString("userPrivateKey");
updateKeys({
encryptedPrivateKey: initialPrivateKey,
});
// Because switchMap is a little to good at its job
await awaitAsync();
// Current architecture requires that provider keys are set before org keys
updateKeys({
providerKeys: {},
});
// Because switchMap is a little to good at its job
await awaitAsync();
// User has their org keys set
updateKeys({
orgKeys: {
[org1Id]: { type: "organization", key: makeEncString("org1Key").encryptedString },
},
});
// Out of band user key update
const updatedUserKey = makeSymmetricCryptoKey<UserKey>(64);
updateKeys({
userKey: updatedUserKey,
});
const emittedValues = await promise;
// They start with no data
expect(emittedValues[0]).toBeNull();
// They get their user key set
expect(emittedValues[1]).toEqual({
userKey: initialUserKey,
orgKeys: null,
});
// Once a private key is set we will attempt org key decryption, even if org keys haven't been set
expect(emittedValues[2]).toEqual({
userKey: initialUserKey,
orgKeys: {},
});
// Will emit again when providers alone are set, but this won't change the output until orgs are set
expect(emittedValues[3]).toEqual({
userKey: initialUserKey,
orgKeys: {},
});
// Expect org keys to get emitted
expect(emittedValues[4]).toEqual({
userKey: initialUserKey,
orgKeys: {
[org1Id]: expect.anything(),
},
});
// Expect out of band user key update
expect(emittedValues[5]).toEqual({
userKey: updatedUserKey,
orgKeys: {
[org1Id]: expect.anything(),
},
});
});
});
});

View File

@@ -1,990 +0,0 @@
import * as bigInt from "big-integer";
import {
NEVER,
Observable,
combineLatest,
firstValueFrom,
forkJoin,
map,
of,
switchMap,
} from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { BaseEncryptedOrganizationKey } from "../../admin-console/models/domain/encrypted-organization-key";
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { AccountService } from "../../auth/abstractions/account.service";
import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { Utils } from "../../platform/misc/utils";
import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state";
import { CsprngArray } from "../../types/csprng";
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
import {
OrgKey,
UserKey,
MasterKey,
ProviderKey,
CipherKey,
UserPrivateKey,
UserPublicKey,
} from "../../types/key";
import { VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import {
CipherDecryptionKeys,
CryptoService as CryptoServiceAbstraction,
UserPrivateKeyDecryptionFailedError,
} from "../abstractions/crypto.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { KeyGenerationService } from "../abstractions/key-generation.service";
import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import { KeySuffixOptions, HashPurpose } from "../enums";
import { convertValues } from "../misc/convert-values";
import { EFFLongWordList } from "../misc/wordlist";
import { EncString, EncryptedString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { ActiveUserState, StateProvider } from "../state";
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state";
import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state";
import {
USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY,
USER_KEY,
} from "./key-state/user-key.state";
export class CryptoService implements CryptoServiceAbstraction {
private readonly activeUserEverHadUserKey: ActiveUserState<boolean>;
readonly everHadUserKey$: Observable<boolean>;
readonly activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
constructor(
protected pinService: PinServiceAbstraction,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected keyGenerationService: KeyGenerationService,
protected cryptoFunctionService: CryptoFunctionService,
protected encryptService: EncryptService,
protected platformUtilService: PlatformUtilsService,
protected logService: LogService,
protected stateService: StateService,
protected accountService: AccountService,
protected stateProvider: StateProvider,
protected kdfConfigService: KdfConfigService,
) {
// User Key
this.activeUserEverHadUserKey = stateProvider.getActive(USER_EVER_HAD_USER_KEY);
this.everHadUserKey$ = this.activeUserEverHadUserKey.state$.pipe(map((x) => x ?? false));
this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe(
switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)),
);
}
async setUserKey(key: UserKey, userId: UserId): Promise<void> {
if (key == null) {
throw new Error("No key provided. Lock the user to clear the key");
}
if (userId == null) {
throw new Error("No userId provided.");
}
// Set userId to ensure we have one for the account status update
await this.stateProvider.setUserState(USER_KEY, key, userId);
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId);
await this.storeAdditionalKeys(key, userId);
}
async setUserKeys(
userKey: UserKey,
encPrivateKey: EncryptedString,
userId: UserId,
): Promise<void> {
if (userKey == null) {
throw new Error("No userKey provided. Lock the user to clear the key");
}
if (encPrivateKey == null) {
throw new Error("No encPrivateKey provided.");
}
if (userId == null) {
throw new Error("No userId provided.");
}
const decryptedPrivateKey = await this.decryptPrivateKey(encPrivateKey, userKey);
if (decryptedPrivateKey == null) {
throw new UserPrivateKeyDecryptionFailedError();
}
await this.setUserKey(userKey, userId);
await this.setPrivateKey(encPrivateKey, userId);
}
async refreshAdditionalKeys(): Promise<void> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
throw new Error("Can only refresh keys while there is an active user.");
}
const key = await this.getUserKey(activeUserId);
await this.setUserKey(key, activeUserId);
}
getInMemoryUserKeyFor$(userId: UserId): Observable<UserKey> {
return this.stateProvider.getUserState$(USER_KEY, userId);
}
async getUserKey(userId?: UserId): Promise<UserKey> {
const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
return userKey;
}
async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
return await this.validateUserKey(masterKey as unknown as UserKey, userId);
}
// TODO: legacy support for user key is no longer needed since we require users to migrate on login
async getUserKeyWithLegacySupport(userId?: UserId): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
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.
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
return masterKey as unknown as UserKey;
}
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
const userKey = await this.getKeyFromStorage(keySuffix, userId);
if (userKey) {
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> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
return false;
}
return await this.hasUserKeyInMemory(userId);
}
async hasUserKeyInMemory(userId?: UserId): Promise<boolean> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
return false;
}
return (await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId))) != null;
}
async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
return (await this.getKeyFromStorage(keySuffix, userId)) != null;
}
async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> {
if (!masterKey) {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
}
if (masterKey == null) {
throw new Error("No Master Key found.");
}
const newUserKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(masterKey, newUserKey.key);
}
/**
* Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key
* @param userId The desired user
*/
private async clearUserKey(userId: UserId): Promise<void> {
if (userId == null) {
// nothing to do
return;
}
// Set userId to ensure we have one for the account status update
await this.stateProvider.setUserState(USER_KEY, null, userId);
await this.clearAllStoredUserKeys(userId);
}
async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> {
if (keySuffix === KeySuffixOptions.Auto) {
// 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
this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
// 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
this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
}
if (keySuffix === KeySuffixOptions.Pin) {
// 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
this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
// 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
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
}
}
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
new EncString(userKeyMasterKey),
userId,
);
}
// TODO: Move to MasterPasswordService
async getOrDeriveMasterKey(password: string, userId?: UserId) {
const [resolvedUserId, email] = await firstValueFrom(
combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe(
map(([activeAccount, accounts]) => {
userId ??= activeAccount?.id;
return [userId, accounts[userId]?.email];
}),
),
);
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId));
return (masterKey ||= await this.makeMasterKey(
password,
email,
await this.kdfConfigService.getKdfConfig(),
));
}
/**
* Derive a master key from a password and email.
*
* @remarks
* Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type.
* TODO: Move to MasterPasswordService
*/
async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey> {
return (await this.keyGenerationService.deriveKeyFromPassword(
password,
email,
KdfConfig,
)) as MasterKey;
}
async encryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: UserKey,
): Promise<[UserKey, EncString]> {
userKey ||= await this.getUserKey();
return await this.buildProtectedSymmetricKey(masterKey, userKey.key);
}
// TODO: move to MasterPasswordService
async hashMasterKey(
password: string,
key: MasterKey,
hashPurpose?: HashPurpose,
): Promise<string> {
if (!key) {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
key = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
}
if (password == null || key == null) {
throw new Error("Invalid parameters.");
}
const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1;
const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, "sha256", iterations);
return Utils.fromBufferToB64(hash);
}
// TODO: move to MasterPasswordService
async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
const storedPasswordHash = await firstValueFrom(
this.masterPasswordService.masterKeyHash$(userId),
);
if (masterPassword != null && storedPasswordHash != null) {
const localKeyHash = await this.hashMasterKey(
masterPassword,
masterKey,
HashPurpose.LocalAuthorization,
);
if (localKeyHash != null && storedPasswordHash === localKeyHash) {
return true;
}
// TODO: remove serverKeyHash check in 1-2 releases after everyone's keyHash has been updated
const serverKeyHash = await this.hashMasterKey(
masterPassword,
masterKey,
HashPurpose.ServerAuthorization,
);
if (serverKeyHash != null && storedPasswordHash === serverKeyHash) {
await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
return true;
}
}
return false;
}
async setOrgKeys(
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
userId: UserId,
): Promise<void> {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).update(() => {
const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {};
orgs.forEach((org) => {
encOrgKeyData[org.id] = {
type: "organization",
key: org.key,
};
});
providerOrgs.forEach((org) => {
encOrgKeyData[org.id] = {
type: "provider",
providerId: org.providerId,
key: org.key,
};
});
return encOrgKeyData;
});
}
async getOrgKey(orgId: OrganizationId): Promise<OrgKey> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
throw new Error("A user must be active to retrieve an org key");
}
const orgKeys = await firstValueFrom(this.orgKeys$(activeUserId));
return orgKeys[orgId];
}
async makeDataEncKey<T extends OrgKey | UserKey>(
key: T,
): Promise<[SymmetricCryptoKey, EncString]> {
if (key == null) {
throw new Error("No key provided");
}
const newSymKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(key, newSymKey.key);
}
private async clearOrgKeys(userId: UserId): Promise<void> {
if (userId == null) {
// nothing to do
return;
}
await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId);
}
async setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).update(() => {
const encProviderKeys: { [providerId: ProviderId]: EncryptedString } = {};
providers.forEach((provider) => {
encProviderKeys[provider.id as ProviderId] = provider.key as EncryptedString;
});
return encProviderKeys;
});
}
// TODO: Deprecate in favor of observable
async getProviderKey(providerId: ProviderId): Promise<ProviderKey> {
if (providerId == null) {
return null;
}
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId));
return providerKeys[providerId] ?? null;
}
private async clearProviderKeys(userId: UserId): Promise<void> {
if (userId == null) {
// nothing to do
return;
}
await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId);
}
// TODO: Make userId required
async makeOrgKey<T extends OrgKey | ProviderKey>(userId?: UserId): Promise<[EncString, T]> {
const shareKey = await this.keyGenerationService.createKey(512);
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
const publicKey = await firstValueFrom(this.userPublicKey$(userId));
const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey);
return [encShareKey, shareKey as T];
}
async setPrivateKey(encPrivateKey: EncryptedString, userId: UserId): Promise<void> {
if (encPrivateKey == null) {
return;
}
await this.stateProvider
.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => encPrivateKey);
}
async getPrivateKey(): Promise<Uint8Array> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
throw new Error("User must be active while attempting to retrieve private key.");
}
return await firstValueFrom(this.userPrivateKey$(activeUserId));
}
// TODO: Make public key required
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
if (publicKey == null) {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
publicKey = await firstValueFrom(this.userPublicKey$(activeUserId));
}
if (publicKey === null) {
throw new Error("No public key available.");
}
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
keyFingerprint,
fingerprintMaterial,
32,
"sha256",
);
return this.hashPhrase(userFingerprint);
}
async makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]> {
if (key == null) {
throw new Error("'key' is a required parameter and must be non-null.");
}
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const publicB64 = Utils.fromBufferToB64(keyPair[0]);
const privateEnc = await this.encryptService.encrypt(keyPair[1], key);
return [publicB64, privateEnc];
}
/**
* Clears the user's key pair
* @param userId The desired user
*/
private async clearKeyPair(userId: UserId): Promise<void[]> {
if (userId == null) {
// nothing to do
return;
}
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
}
async clearPinKeys(userId?: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("Cannot clear PIN keys, no user Id resolved.");
}
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
await this.pinService.clearUserKeyEncryptedPin(userId);
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
}
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
return await this.keyGenerationService.deriveKeyFromMaterial(
keyMaterial,
"bitwarden-send",
"send",
);
}
async makeCipherKey(): Promise<CipherKey> {
return (await this.keyGenerationService.createKey(512)) as CipherKey;
}
async clearKeys(userId?: UserId): Promise<any> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("Cannot clear keys, no user Id resolved.");
}
await this.masterPasswordService.clearMasterKeyHash(userId);
await this.clearUserKey(userId);
await this.clearOrgKeys(userId);
await this.clearProviderKeys(userId);
await this.clearKeyPair(userId);
await this.clearPinKeys(userId);
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
}
// EFForg/OpenWireless
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
async randomNumber(min: number, max: number): Promise<number> {
let rval = 0;
const range = max - min + 1;
const bitsNeeded = Math.ceil(Math.log2(range));
if (bitsNeeded > 53) {
throw new Error("We cannot generate numbers larger than 53 bits.");
}
const bytesNeeded = Math.ceil(bitsNeeded / 8);
const mask = Math.pow(2, bitsNeeded) - 1;
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
// Fill a byte array with N random numbers
const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded));
let p = (bytesNeeded - 1) * 8;
for (let i = 0; i < bytesNeeded; i++) {
rval += byteArray[i] * Math.pow(2, p);
p -= 8;
}
// Use & to apply the mask and reduce the number of recursive lookups
rval = rval & mask;
if (rval >= range) {
// Integer out of acceptable range
return this.randomNumber(min, max);
}
// Return an integer that falls within the range
return min + rval;
}
// ---HELPERS---
async validateUserKey(key: UserKey, userId: UserId): Promise<boolean> {
if (!key) {
return false;
}
try {
const encPrivateKey = await firstValueFrom(
this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$,
);
if (encPrivateKey == null) {
return false;
}
// Can decrypt private key
const privateKey = await this.decryptPrivateKey(encPrivateKey, key);
if (privateKey == null) {
// failed to decrypt
return false;
}
// Can successfully derive public key
const publicKey = await this.derivePublicKey(privateKey);
if (publicKey == null) {
// failed to decrypt
return false;
}
} catch (e) {
return false;
}
return true;
}
/**
* Initialize all necessary crypto keys needed for a new account.
* Warning! This completely replaces any existing keys!
*/
async initAccount(): Promise<{
userKey: UserKey;
publicKey: string;
privateKey: EncString;
}> {
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
throw new Error("Cannot initilize an account if one is not active.");
}
// Verify user key doesn't exist
const existingUserKey = await this.getUserKey(activeUserId);
if (existingUserKey != null) {
this.logService.error("Tried to initialize account with existing user key.");
throw new Error("Cannot initialize account, keys already exist.");
}
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
await this.setUserKey(userKey, activeUserId);
await this.stateProvider
.getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => privateKey.encryptedString);
return {
userKey,
publicKey,
privateKey,
};
}
/**
* 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
*/
protected async storeAdditionalKeys(key: UserKey, userId: UserId) {
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
if (storeAuto) {
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
} else {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
}
await this.clearDeprecatedKeys(KeySuffixOptions.Auto, userId);
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
if (storePin) {
// Decrypt userKeyEncryptedPin with user key
const pin = await this.encryptService.decryptToUtf8(
await this.pinService.getUserKeyEncryptedPin(userId),
key,
);
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
pin,
key,
userId,
);
const noPreExistingPersistentKey =
(await this.pinService.getPinKeyEncryptedUserKeyPersistent(userId)) == null;
await this.pinService.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey,
noPreExistingPersistentKey,
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.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
}
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: UserId) {
let shouldStoreKey = false;
switch (keySuffix) {
case KeySuffixOptions.Auto: {
// TODO: Sharing the UserKeyDefinition is temporary to get around a circ dep issue between
// the VaultTimeoutSettingsSvc and this service.
// This should be fixed as part of the PM-7082 - Auto Key Service work.
const vaultTimeout = await firstValueFrom(
this.stateProvider.getUserState$(VAULT_TIMEOUT, userId),
);
shouldStoreKey = vaultTimeout == VaultTimeoutStringType.Never;
break;
}
case KeySuffixOptions.Pin: {
const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId);
shouldStoreKey = !!userKeyEncryptedPin;
break;
}
}
return shouldStoreKey;
}
protected async getKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: UserId,
): Promise<UserKey> {
if (keySuffix === KeySuffixOptions.Auto) {
const userKey = await this.stateService.getUserKeyAutoUnlock({ userId: userId });
if (userKey) {
return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey)) as UserKey;
}
}
return null;
}
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
const entropyPerWord = Math.log(EFFLongWordList.length) / Math.log(2);
let numWords = Math.ceil(minimumEntropy / entropyPerWord);
const hashArr = Array.from(new Uint8Array(hash));
const entropyAvailable = hashArr.length * 4;
if (numWords * entropyPerWord > entropyAvailable) {
throw new Error("Output entropy of hash function is too small");
}
const phrase: string[] = [];
let hashNumber = bigInt.fromArray(hashArr, 256);
while (numWords--) {
const remainder = hashNumber.mod(EFFLongWordList.length);
hashNumber = hashNumber.divide(EFFLongWordList.length);
phrase.push(EFFLongWordList[remainder as any]);
}
return phrase;
}
private async buildProtectedSymmetricKey<T extends SymmetricCryptoKey>(
encryptionKey: SymmetricCryptoKey,
newSymKey: Uint8Array,
): Promise<[T, EncString]> {
let protectedSymKey: EncString = null;
if (encryptionKey.key.byteLength === 32) {
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
} else if (encryptionKey.key.byteLength === 64) {
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
} else {
throw new Error("Invalid key size.");
}
return [new SymmetricCryptoKey(newSymKey) as T, protectedSymKey];
}
// --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.
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475)
async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) {
if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
} else if (keySuffix === KeySuffixOptions.Pin) {
await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
}
}
userKey$(userId: UserId): Observable<UserKey> {
return this.stateProvider.getUser(userId, USER_KEY).state$;
}
private userKeyWithLegacySupport$(userId: UserId) {
return this.userKey$(userId).pipe(
switchMap((userKey) => {
if (userKey != null) {
return of(userKey);
}
// Legacy path
return this.masterPasswordService.masterKey$(userId).pipe(
switchMap(async (masterKey) => {
if (!(await this.validateUserKey(masterKey as unknown as UserKey, userId))) {
// We don't have a UserKey or a valid MasterKey
return null;
}
// The master key is valid meaning, the org keys and such are encrypted with this key
return masterKey as unknown as UserKey;
}),
);
}),
);
}
userPublicKey$(userId: UserId) {
return this.userPrivateKey$(userId).pipe(
switchMap(async (pk) => await this.derivePublicKey(pk)),
);
}
private async derivePublicKey(privateKey: UserPrivateKey) {
if (privateKey == null) {
return null;
}
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
}
userPrivateKey$(userId: UserId): Observable<UserPrivateKey> {
return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey));
}
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
}
userPrivateKeyWithLegacySupport$(userId: UserId): Observable<UserPrivateKey> {
return this.userPrivateKeyHelper$(userId, true).pipe(map((keys) => keys?.userPrivateKey));
}
private userPrivateKeyHelper$(userId: UserId, legacySupport: boolean) {
const userKey$ = legacySupport ? this.userKeyWithLegacySupport$(userId) : this.userKey$(userId);
return userKey$.pipe(
switchMap((userKey) => {
if (userKey == null) {
return of(null);
}
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$.pipe(
switchMap(
async (encryptedPrivateKey) =>
await this.decryptPrivateKey(encryptedPrivateKey, userKey),
),
// Combine outerscope info with user private key
map((userPrivateKey) => ({
userKey,
userPrivateKey,
})),
);
}),
);
}
private async decryptPrivateKey(encryptedPrivateKey: EncryptedString, key: SymmetricCryptoKey) {
if (encryptedPrivateKey == null) {
return null;
}
return (await this.encryptService.decryptToBytes(
new EncString(encryptedPrivateKey),
key,
)) as UserPrivateKey;
}
providerKeys$(userId: UserId) {
return this.userPrivateKey$(userId).pipe(
switchMap((userPrivateKey) => {
if (userPrivateKey == null) {
return of(null);
}
return this.providerKeysHelper$(userId, userPrivateKey);
}),
);
}
/**
* A helper for decrypting provider keys that requires a user id and that users decrypted private key
* this is helpful for when you may have already grabbed the user private key and don't want to redo
* that work to get the provider keys.
*/
private providerKeysHelper$(
userId: UserId,
userPrivateKey: UserPrivateKey,
): Observable<Record<ProviderId, ProviderKey>> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).state$.pipe(
// Convert each value in the record to it's own decryption observable
convertValues(async (_, value) => {
const decrypted = await this.encryptService.rsaDecrypt(
new EncString(value),
userPrivateKey,
);
return new SymmetricCryptoKey(decrypted) as ProviderKey;
}),
// switchMap since there are no side effects
switchMap((encryptedProviderKeys) => {
if (encryptedProviderKeys == null) {
return of(null);
}
// Can't give an empty record to forkJoin
if (Object.keys(encryptedProviderKeys).length === 0) {
return of({});
}
return forkJoin(encryptedProviderKeys);
}),
);
}
orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null> {
return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys));
}
encryptedOrgKeys$(
userId: UserId,
): Observable<Record<OrganizationId, EncryptedOrganizationKeyData>> {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$;
}
cipherDecryptionKeys$(
userId: UserId,
legacySupport: boolean = false,
): Observable<CipherDecryptionKeys | null> {
return this.userPrivateKeyHelper$(userId, legacySupport).pipe(
switchMap((userKeys) => {
if (userKeys == null) {
return of(null);
}
const userPrivateKey = userKeys.userPrivateKey;
if (userPrivateKey == null) {
// We can't do any org based decryption
return of({ userKey: userKeys.userKey, orgKeys: null });
}
return combineLatest([
this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$,
this.providerKeysHelper$(userId, userPrivateKey),
]).pipe(
switchMap(async ([encryptedOrgKeys, providerKeys]) => {
const result: Record<OrganizationId, OrgKey> = {};
for (const orgId of Object.keys(encryptedOrgKeys ?? {}) as OrganizationId[]) {
if (result[orgId] != null) {
continue;
}
const encrypted = BaseEncryptedOrganizationKey.fromData(encryptedOrgKeys[orgId]);
let decrypted: OrgKey;
if (BaseEncryptedOrganizationKey.isProviderEncrypted(encrypted)) {
decrypted = await encrypted.decrypt(this.encryptService, providerKeys);
} else {
decrypted = await encrypted.decrypt(this.encryptService, userPrivateKey);
}
result[orgId] = decrypted;
}
return result;
}),
// Combine them back together
map((orgKeys) => ({ userKey: userKeys.userKey, orgKeys: orgKeys })),
);
}),
);
}
}

View File

@@ -1,6 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, of } from "rxjs";
import { KeyService } from "@bitwarden/key-management";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { ApiService } from "../../../abstractions/api.service";
@@ -9,7 +10,6 @@ import { KdfConfigService } from "../../../auth/abstractions/kdf-config.service"
import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config";
import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
import { CryptoService } from "../../abstractions/crypto.service";
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
@@ -25,7 +25,7 @@ describe("DefaultSdkService", () => {
let platformUtilsService!: MockProxy<PlatformUtilsService>;
let accountService!: MockProxy<AccountService>;
let kdfConfigService!: MockProxy<KdfConfigService>;
let cryptoService!: MockProxy<CryptoService>;
let keyService!: MockProxy<KeyService>;
let apiService!: MockProxy<ApiService>;
let service!: DefaultSdkService;
@@ -37,7 +37,7 @@ describe("DefaultSdkService", () => {
platformUtilsService = mock<PlatformUtilsService>();
accountService = mock<AccountService>();
kdfConfigService = mock<KdfConfigService>();
cryptoService = mock<CryptoService>();
keyService = mock<KeyService>();
apiService = mock<ApiService>();
// Can't use `of(mock<Environment>())` for some reason
@@ -49,7 +49,7 @@ describe("DefaultSdkService", () => {
platformUtilsService,
accountService,
kdfConfigService,
cryptoService,
keyService,
apiService,
);
@@ -68,13 +68,13 @@ describe("DefaultSdkService", () => {
kdfConfigService.getKdfConfig$
.calledWith(userId)
.mockReturnValue(of(new PBKDF2KdfConfig()));
cryptoService.userKey$
keyService.userKey$
.calledWith(userId)
.mockReturnValue(of(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey));
cryptoService.userEncryptedPrivateKey$
keyService.userEncryptedPrivateKey$
.calledWith(userId)
.mockReturnValue(of("private-key" as EncryptedString));
cryptoService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({}));
keyService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({}));
});
it("creates an SDK client when called the first time", async () => {
@@ -115,7 +115,7 @@ describe("DefaultSdkService", () => {
it("destroys the SDK client when the userKey is unset (i.e. lock or logout)", async () => {
const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey);
cryptoService.userKey$.calledWith(userId).mockReturnValue(userKey$);
keyService.userKey$.calledWith(userId).mockReturnValue(userKey$);
const subject = new BehaviorSubject(undefined);
service.userClient$(userId).subscribe(subject);

View File

@@ -10,6 +10,7 @@ import {
switchMap,
} from "rxjs";
import { KeyService } from "@bitwarden/key-management";
import {
BitwardenClient,
ClientSettings,
@@ -25,7 +26,6 @@ import { KdfConfig } from "../../../auth/models/domain/kdf-config";
import { DeviceType } from "../../../enums/device-type.enum";
import { OrganizationId, UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
import { CryptoService } from "../../abstractions/crypto.service";
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
@@ -57,7 +57,7 @@ export class DefaultSdkService implements SdkService {
private platformUtilsService: PlatformUtilsService,
private accountService: AccountService,
private kdfConfigService: KdfConfigService,
private cryptoService: CryptoService,
private keyService: KeyService,
private apiService: ApiService, // Yes we shouldn't import ApiService, but it's temporary
private userAgent: string = null,
) {}
@@ -73,11 +73,11 @@ export class DefaultSdkService implements SdkService {
distinctUntilChanged(),
);
const kdfParams$ = this.kdfConfigService.getKdfConfig$(userId).pipe(distinctUntilChanged());
const privateKey$ = this.cryptoService
const privateKey$ = this.keyService
.userEncryptedPrivateKey$(userId)
.pipe(distinctUntilChanged());
const userKey$ = this.cryptoService.userKey$(userId).pipe(distinctUntilChanged());
const orgKeys$ = this.cryptoService.encryptedOrgKeys$(userId).pipe(
const userKey$ = this.keyService.userKey$(userId).pipe(distinctUntilChanged());
const orgKeys$ = this.keyService.encryptedOrgKeys$(userId).pipe(
distinctUntilChanged(compareValues), // The upstream observable emits different objects with the same values
);

View File

@@ -1,5 +1,6 @@
import { mock } from "jest-mock-extended";
import { DefaultKeyService } from "../../../../key-management/src/key.service";
import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid";
import { UserKey } from "../../types/key";
@@ -7,7 +8,6 @@ import { KeySuffixOptions } from "../enums";
import { Utils } from "../misc/utils";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { CryptoService } from "./crypto.service";
import { UserAutoUnlockKeyService } from "./user-auto-unlock-key.service";
describe("UserAutoUnlockKeyService", () => {
@@ -15,10 +15,10 @@ describe("UserAutoUnlockKeyService", () => {
const mockUserId = Utils.newGuid() as UserId;
const cryptoService = mock<CryptoService>();
const keyService = mock<DefaultKeyService>();
beforeEach(() => {
userAutoUnlockKeyService = new UserAutoUnlockKeyService(cryptoService);
userAutoUnlockKeyService = new UserAutoUnlockKeyService(keyService);
});
describe("setUserKeyInMemoryIfAutoUserKeySet", () => {
@@ -27,25 +27,22 @@ describe("UserAutoUnlockKeyService", () => {
await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(null);
// Assert
expect(cryptoService.getUserKeyFromStorage).not.toHaveBeenCalled();
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
expect(keyService.getUserKeyFromStorage).not.toHaveBeenCalled();
expect(keyService.setUserKey).not.toHaveBeenCalled();
});
it("does nothing if the autoUserKey is null", async () => {
// Arrange
const userId = mockUserId;
cryptoService.getUserKeyFromStorage.mockResolvedValue(null);
keyService.getUserKeyFromStorage.mockResolvedValue(null);
// Act
await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(userId);
// Assert
expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith(
KeySuffixOptions.Auto,
userId,
);
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
expect(keyService.getUserKeyFromStorage).toHaveBeenCalledWith(KeySuffixOptions.Auto, userId);
expect(keyService.setUserKey).not.toHaveBeenCalled();
});
it("sets the user key in memory if the autoUserKey is not null", async () => {
@@ -55,17 +52,14 @@ describe("UserAutoUnlockKeyService", () => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
const mockAutoUserKey: UserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
cryptoService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey);
keyService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey);
// Act
await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(userId);
// Assert
expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith(
KeySuffixOptions.Auto,
userId,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId);
expect(keyService.getUserKeyFromStorage).toHaveBeenCalledWith(KeySuffixOptions.Auto, userId);
expect(keyService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId);
});
});
});

View File

@@ -1,15 +1,15 @@
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { UserId } from "../../types/guid";
import { CryptoService } from "../abstractions/crypto.service";
import { KeySuffixOptions } from "../enums";
// TODO: this is a half measure improvement which allows us to reduce some side effects today (cryptoService.getUserKey setting user key in memory if auto key exists)
// but ideally, in the future, we would be able to put this logic into the cryptoService
// TODO: this is a half measure improvement which allows us to reduce some side effects today (keyService.getUserKey setting user key in memory if auto key exists)
// but ideally, in the future, we would be able to put this logic into the keyService
// after the vault timeout settings service is transitioned to state provider so that
// the getUserKey logic can simply go to the correct location based on the vault timeout settings
// similar to the TokenService (it would either go to secure storage for the auto user key or memory for the user key)
export class UserAutoUnlockKeyService {
constructor(private cryptoService: CryptoService) {}
constructor(private keyService: KeyService) {}
/**
* The presence of the user key in memory dictates whether the user's vault is locked or unlocked.
@@ -23,16 +23,13 @@ export class UserAutoUnlockKeyService {
return false;
}
const autoUserKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Auto,
userId,
);
const autoUserKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Auto, userId);
if (autoUserKey == null) {
return false;
}
await this.cryptoService.setUserKey(autoUserKey, userId);
await this.keyService.setUserKey(autoUserKey, userId);
return true;
}
}

View File

@@ -8,6 +8,7 @@ import {
import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { LogoutReason } from "../../../../auth/src/common/types";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { ApiService } from "../../abstractions/api.service";
import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
@@ -41,7 +42,6 @@ import { CipherData } from "../../vault/models/data/cipher.data";
import { FolderData } from "../../vault/models/data/folder.data";
import { CipherResponse } from "../../vault/models/response/cipher.response";
import { FolderResponse } from "../../vault/models/response/folder.response";
import { CryptoService } from "../abstractions/crypto.service";
import { LogService } from "../abstractions/log.service";
import { StateService } from "../abstractions/state.service";
import { MessageSender } from "../messaging";
@@ -60,7 +60,7 @@ export class DefaultSyncService extends CoreSyncService {
private domainSettingsService: DomainSettingsService,
folderService: InternalFolderService,
cipherService: CipherService,
private cryptoService: CryptoService,
private keyService: KeyService,
collectionService: CollectionService,
messageSender: MessageSender,
private policyService: InternalPolicyService,
@@ -178,10 +178,10 @@ export class DefaultSyncService extends CoreSyncService {
throw new Error("Stamp has changed");
}
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, response.id);
await this.cryptoService.setPrivateKey(response.privateKey, response.id);
await this.cryptoService.setProviderKeys(response.providers, response.id);
await this.cryptoService.setOrgKeys(
await this.keyService.setMasterKeyEncryptedUserKey(response.key, response.id);
await this.keyService.setPrivateKey(response.privateKey, response.id);
await this.keyService.setProviderKeys(response.providers, response.id);
await this.keyService.setOrgKeys(
response.organizations,
response.providerOrganizations,
response.id,

View File

@@ -10,13 +10,13 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { BiometricStateService } from "@bitwarden/key-management";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { Policy } from "../../admin-console/models/domain/policy";
import { TokenService } from "../../auth/abstractions/token.service";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service";
import {
VAULT_TIMEOUT,
@@ -30,7 +30,7 @@ describe("VaultTimeoutSettingsService", () => {
let accountService: FakeAccountService;
let pinService: MockProxy<PinServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>;
let keyService: MockProxy<KeyService>;
let tokenService: MockProxy<TokenService>;
let policyService: MockProxy<PolicyService>;
const biometricStateService = mock<BiometricStateService>();
@@ -46,7 +46,7 @@ describe("VaultTimeoutSettingsService", () => {
accountService = mockAccountServiceWith(mockUserId);
pinService = mock<PinServiceAbstraction>();
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
cryptoService = mock<CryptoService>();
keyService = mock<KeyService>();
tokenService = mock<TokenService>();
policyService = mock<PolicyService>();
@@ -342,7 +342,7 @@ describe("VaultTimeoutSettingsService", () => {
stateProvider.singleUser.getFake(mockUserId, VAULT_TIMEOUT).nextMock,
).toHaveBeenCalledWith(timeout);
expect(cryptoService.refreshAdditionalKeys).toHaveBeenCalled();
expect(keyService.refreshAdditionalKeys).toHaveBeenCalled();
});
it("should clear the tokens when the timeout is not never and the action is log out", async () => {
@@ -377,7 +377,7 @@ describe("VaultTimeoutSettingsService", () => {
accountService,
pinService,
userDecryptionOptionsService,
cryptoService,
keyService,
tokenService,
policyService,
biometricStateService,

View File

@@ -19,6 +19,7 @@ import {
} from "@bitwarden/auth/common";
import { BiometricStateService } from "@bitwarden/key-management";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../admin-console/enums";
@@ -26,7 +27,6 @@ import { Policy } from "../../admin-console/models/domain/policy";
import { AccountService } from "../../auth/abstractions/account.service";
import { TokenService } from "../../auth/abstractions/token.service";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service";
import { StateProvider } from "../../platform/state";
import { UserId } from "../../types/guid";
@@ -39,7 +39,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private cryptoService: CryptoService,
private keyService: KeyService,
private tokenService: TokenService,
private policyService: PolicyService,
private biometricStateService: BiometricStateService,
@@ -87,7 +87,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
clientSecret,
]);
await this.cryptoService.refreshAdditionalKeys();
await this.keyService.refreshAdditionalKeys();
}
availableVaultTimeoutActions$(userId?: string): Observable<VaultTimeoutAction[]> {
@@ -287,7 +287,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
}
async clear(userId?: string): Promise<void> {
await this.cryptoService.clearPinKeys(userId);
await this.keyService.clearPinKeys(userId);
}
private async userHasMasterPassword(userId: string): Promise<boolean> {

View File

@@ -3,8 +3,8 @@ import { mock } from "jest-mock-extended";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "../../../../../../key-management/src/abstractions/key.service";
import { makeStaticByteArray, mockEnc } from "../../../../../spec";
import { CryptoService } from "../../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../../platform/abstractions/encrypt.service";
import { ContainerService } from "../../../../platform/services/container.service";
import { SendType } from "../../enums/send-type";
@@ -111,14 +111,14 @@ describe("Send", () => {
send.hideEmail = true;
const encryptService = mock<EncryptService>();
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
encryptService.decryptToBytes
.calledWith(send.key, userKey)
.mockResolvedValue(makeStaticByteArray(32));
cryptoService.makeSendKey.mockResolvedValue("cryptoKey" as any);
cryptoService.getUserKey.mockResolvedValue(userKey);
keyService.makeSendKey.mockResolvedValue("cryptoKey" as any);
keyService.getUserKey.mockResolvedValue(userKey);
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
const view = await send.decrypt();

View File

@@ -72,13 +72,13 @@ export class Send extends Domain {
async decrypt(): Promise<SendView> {
const model = new SendView(this);
const cryptoService = Utils.getContainerService().getCryptoService();
const keyService = Utils.getContainerService().getKeyService();
const encryptService = Utils.getContainerService().getEncryptService();
try {
const sendKeyEncryptionKey = await cryptoService.getUserKey();
const sendKeyEncryptionKey = await keyService.getUserKey();
model.key = await encryptService.decryptToBytes(this.key, sendKeyEncryptionKey);
model.cryptoKey = await cryptoService.makeSendKey(model.key);
model.cryptoKey = await keyService.makeSendKey(model.key);
} catch (e) {
// TODO: error?
}

View File

@@ -4,6 +4,7 @@ import { firstValueFrom, of } from "rxjs";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import {
FakeAccountService,
FakeActiveUserState,
@@ -11,7 +12,6 @@ import {
awaitAsync,
mockAccountServiceWith,
} from "../../../../spec";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
@@ -40,7 +40,7 @@ import {
} from "./test-data/send-tests.data";
describe("SendService", () => {
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const i18nService = mock<I18nService>();
const keyGenerationService = mock<KeyGenerationService>();
const encryptService = mock<EncryptService>();
@@ -65,7 +65,7 @@ describe("SendService", () => {
get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })),
});
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
accountService.activeAccountSubject.next({
id: mockUserId,
@@ -84,7 +84,7 @@ describe("SendService", () => {
decryptedState.nextState([testSendViewData("1", "Test Send")]);
sendService = new SendService(
cryptoService,
keyService,
i18nService,
keyGenerationService,
sendStateProvider,

View File

@@ -1,7 +1,7 @@
import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
@@ -37,7 +37,7 @@ export class SendService implements InternalSendServiceAbstraction {
);
constructor(
private cryptoService: CryptoService,
private keyService: KeyService,
private i18nService: I18nService,
private keyGenerationService: KeyGenerationService,
private stateProvider: SendStateProvider,
@@ -77,7 +77,7 @@ export class SendService implements InternalSendServiceAbstraction {
send.password = passwordKey.keyB64;
}
if (key == null) {
key = await this.cryptoService.getUserKey();
key = await this.keyService.getUserKey();
}
send.key = await this.encryptService.encrypt(model.key, key);
send.name = await this.encryptService.encrypt(model.name, model.cryptoKey);
@@ -197,7 +197,7 @@ export class SendService implements InternalSendServiceAbstraction {
}
decSends = [];
const hasKey = await this.cryptoService.hasUserKey();
const hasKey = await this.keyService.hasUserKey();
if (!hasKey) {
throw new Error("No user key found.");
}
@@ -322,7 +322,7 @@ export class SendService implements InternalSendServiceAbstraction {
key: SymmetricCryptoKey,
): Promise<[EncString, EncArrayBuffer]> {
if (key == null) {
key = await this.cryptoService.getUserKey();
key = await this.keyService.getUserKey();
}
const encFileName = await this.encryptService.encrypt(fileName, key);
const encFileData = await this.encryptService.encryptToBytes(new Uint8Array(data), key);

View File

@@ -1,7 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -57,17 +57,14 @@ describe("Attachment", () => {
});
describe("decrypt", () => {
let cryptoService: MockProxy<CryptoService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
beforeEach(() => {
cryptoService = mock<CryptoService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService,
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
});
it("expected output", async () => {
@@ -101,32 +98,32 @@ describe("Attachment", () => {
attachment.key = mock<EncString>();
});
it("uses the provided key without depending on CryptoService", async () => {
it("uses the provided key without depending on KeyService", async () => {
const providedKey = mock<SymmetricCryptoKey>();
await attachment.decrypt(null, providedKey);
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey);
});
it("gets an organization key if required", async () => {
const orgKey = mock<OrgKey>();
cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
await attachment.decrypt("orgId", null);
expect(cryptoService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, orgKey);
});
it("gets the user's decryption key if required", async () => {
const userKey = mock<UserKey>();
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
await attachment.decrypt(null, null);
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalled();
expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey);
});
});

View File

@@ -68,10 +68,10 @@ export class Attachment extends Domain {
}
private async getKeyForDecryption(orgId: string) {
const cryptoService = Utils.getContainerService().getCryptoService();
const keyService = Utils.getContainerService().getKeyService();
return orgId != null
? await cryptoService.getOrgKey(orgId)
: await cryptoService.getUserKeyWithLegacySupport();
? await keyService.getOrgKey(orgId)
: await keyService.getUserKeyWithLegacySupport();
}
toAttachmentData(): AttachmentData {

View File

@@ -3,9 +3,9 @@ import { Jsonify } from "type-fest";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string";
import { ContainerService } from "../../../platform/services/container.service";
@@ -237,16 +237,13 @@ describe("Cipher DTO", () => {
login.decrypt.mockResolvedValue(loginView);
cipher.login = login;
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>();
encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64));
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService,
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
@@ -357,16 +354,13 @@ describe("Cipher DTO", () => {
cipher.secureNote.type = SecureNoteType.Generic;
cipher.key = mockEnc("EncKey");
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>();
encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64));
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService,
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
@@ -495,16 +489,13 @@ describe("Cipher DTO", () => {
card.decrypt.mockResolvedValue(cardView);
cipher.card = card;
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>();
encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64));
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService,
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
@@ -657,16 +648,13 @@ describe("Cipher DTO", () => {
identity.decrypt.mockResolvedValue(identityView);
cipher.identity = identity;
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>();
encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64));
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService,
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),

View File

@@ -70,7 +70,7 @@ describe("LoginUri", () => {
encryptService = mock();
global.bitwardenContainerService = {
getEncryptService: () => encryptService,
getCryptoService: () => null,
getKeyService: () => null,
};
});

View File

@@ -47,8 +47,8 @@ export class LoginUri extends Domain {
return false;
}
const cryptoService = Utils.getContainerService().getEncryptService();
const localChecksum = await cryptoService.hash(clearTextUri, "sha256");
const keyService = Utils.getContainerService().getEncryptService();
const localChecksum = await keyService.hash(clearTextUri, "sha256");
const remoteChecksum = await this.uriChecksum.decrypt(orgId, encKey);
return remoteChecksum === localChecksum;

View File

@@ -3,6 +3,10 @@ import { BehaviorSubject, map, of } from "rxjs";
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
import {
CipherDecryptionKeys,
KeyService,
} from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { makeStaticByteArray } from "../../../spec/utils";
@@ -12,7 +16,6 @@ import { AutofillSettingsService } from "../../autofill/services/autofill-settin
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { CipherDecryptionKeys, CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
@@ -107,7 +110,7 @@ const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
describe("Cipher Service", () => {
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const stateService = mock<StateService>();
const autofillSettingsService = mock<AutofillSettingsService>();
const domainSettingsService = mock<DomainSettingsService>();
@@ -130,10 +133,10 @@ describe("Cipher Service", () => {
encryptService.encryptToBytes.mockReturnValue(Promise.resolve(ENCRYPTED_BYTES));
encryptService.encrypt.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT)));
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
cipherService = new CipherService(
cryptoService,
keyService,
domainSettingsService,
apiService,
i18nService,
@@ -159,10 +162,10 @@ describe("Cipher Service", () => {
it("should upload encrypted file contents with save attachments", async () => {
const fileName = "filename";
const fileData = new Uint8Array(10);
cryptoService.getOrgKey.mockReturnValue(
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
cryptoService.makeDataEncKey.mockReturnValue(
keyService.makeDataEncKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32))),
);
@@ -271,7 +274,7 @@ describe("Cipher Service", () => {
encryptService.decryptToBytes.mockReturnValue(Promise.resolve(makeStaticByteArray(64)));
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
cryptoService.makeCipherKey.mockReturnValue(
keyService.makeCipherKey.mockReturnValue(
Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey),
);
encryptService.encrypt.mockImplementation(encryptText);
@@ -286,7 +289,7 @@ describe("Cipher Service", () => {
{ uri: "uri", match: UriMatchStrategy.RegularExpression } as LoginUriView,
];
cryptoService.getOrgKey.mockReturnValue(
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
@@ -306,7 +309,7 @@ describe("Cipher Service", () => {
it("is null when feature flag is false", async () => {
configService.getFeatureFlag.mockResolvedValue(false);
cryptoService.getOrgKey.mockReturnValue(
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
const cipher = await cipherService.encrypt(cipherView, userId);
@@ -330,7 +333,7 @@ describe("Cipher Service", () => {
it("is not called when feature flag is false", async () => {
configService.getFeatureFlag.mockResolvedValue(false);
cryptoService.getOrgKey.mockReturnValue(
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
@@ -341,7 +344,7 @@ describe("Cipher Service", () => {
it("is called when feature flag is true", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
cryptoService.getOrgKey.mockReturnValue(
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
@@ -368,7 +371,7 @@ describe("Cipher Service", () => {
const keys = {
userKey: originalUserKey,
} as CipherDecryptionKeys;
cryptoService.cipherDecryptionKeys$.mockReturnValue(of(keys));
keyService.cipherDecryptionKeys$.mockReturnValue(of(keys));
const cipher1 = new CipherView(cipherObj);
cipher1.id = "Cipher 1";
@@ -387,7 +390,7 @@ describe("Cipher Service", () => {
encryptedKey = new EncString("Re-encrypted Cipher Key");
encryptService.encrypt.mockResolvedValue(encryptedKey);
cryptoService.makeCipherKey.mockResolvedValue(
keyService.makeCipherKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(32)) as CipherKey,
);
});

View File

@@ -15,6 +15,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
@@ -24,7 +25,6 @@ import { ErrorResponse } from "../../models/response/error.response";
import { ListResponse } from "../../models/response/list.response";
import { View } from "../../models/view/view";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
@@ -112,7 +112,7 @@ export class CipherService implements CipherServiceAbstraction {
private addEditCipherInfoState: ActiveUserState<AddEditCipherInfo>;
constructor(
private cryptoService: CryptoService,
private keyService: KeyService,
private domainSettingsService: DomainSettingsService,
private apiService: ApiService,
private i18nService: I18nService,
@@ -400,7 +400,7 @@ export class CipherService implements CipherServiceAbstraction {
}
private async decryptCiphers(ciphers: Cipher[], userId: UserId) {
const keys = await firstValueFrom(this.cryptoService.cipherDecryptionKeys$(userId, true));
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
// return early if there are no keys to decrypt with
@@ -550,7 +550,7 @@ export class CipherService implements CipherServiceAbstraction {
}
const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr)));
const key = await this.cryptoService.getOrgKey(organizationId);
const key = await this.keyService.getOrgKey(organizationId);
let decCiphers: CipherView[] = [];
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
decCiphers = await this.bulkEncryptService.decryptItems(ciphers, key);
@@ -848,7 +848,7 @@ export class CipherService implements CipherServiceAbstraction {
const encFileName = await this.encryptService.encrypt(filename, cipherEncKey);
const dataEncKey = await this.cryptoService.makeDataEncKey(cipherEncKey);
const dataEncKey = await this.keyService.makeDataEncKey(cipherEncKey);
const encData = await this.encryptService.encryptToBytes(new Uint8Array(data), dataEncKey[0]);
const response = await this.cipherFileUploadService.upload(
@@ -1245,8 +1245,8 @@ export class CipherService implements CipherServiceAbstraction {
async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> {
return (
(await this.cryptoService.getOrgKey(cipher.organizationId)) ||
((await this.cryptoService.getUserKeyWithLegacySupport(userId)) as UserKey)
(await this.keyService.getOrgKey(cipher.organizationId)) ||
((await this.keyService.getUserKeyWithLegacySupport(userId)) as UserKey)
);
}
@@ -1294,7 +1294,7 @@ export class CipherService implements CipherServiceAbstraction {
// In the case of a cipher that is being shared with an organization, we want to decrypt the
// cipher key with the user's key and then re-encrypt it with the organization's key.
private async encryptSharedCipher(model: CipherView, userId: UserId): Promise<Cipher> {
const keyForCipherKeyDecryption = await this.cryptoService.getUserKeyWithLegacySupport(userId);
const keyForCipherKeyDecryption = await this.keyService.getUserKeyWithLegacySupport(userId);
return await this.encrypt(model, userId, null, keyForCipherKeyDecryption);
}
@@ -1371,14 +1371,14 @@ export class CipherService implements CipherServiceAbstraction {
const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
const userKey = await this.cryptoService.getUserKeyWithLegacySupport(activeUserId.id);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id);
const decBuf = await this.encryptService.decryptToBytes(encBuf, userKey);
let encKey: UserKey | OrgKey;
encKey = await this.cryptoService.getOrgKey(organizationId);
encKey ||= (await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey;
encKey = await this.keyService.getOrgKey(organizationId);
encKey ||= (await this.keyService.getUserKeyWithLegacySupport()) as UserKey;
const dataEncKey = await this.cryptoService.makeDataEncKey(encKey);
const dataEncKey = await this.keyService.makeDataEncKey(encKey);
const encFileName = await this.encryptService.encrypt(attachmentView.fileName, encKey);
const encData = await this.encryptService.encryptToBytes(new Uint8Array(decBuf), dataEncKey[0]);
@@ -1679,7 +1679,7 @@ export class CipherService implements CipherServiceAbstraction {
// First, we get the key for cipher key encryption, in its decrypted form
let decryptedCipherKey: SymmetricCryptoKey;
if (cipher.key == null) {
decryptedCipherKey = await this.cryptoService.makeCipherKey();
decryptedCipherKey = await this.keyService.makeCipherKey();
} else {
decryptedCipherKey = new SymmetricCryptoKey(
await this.encryptService.decryptToBytes(cipher.key, keyForCipherKeyDecryption),

View File

@@ -1,11 +1,11 @@
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { makeStaticByteArray } from "../../../../spec";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service";
import { FakeActiveUserState } from "../../../../spec/fake-state";
import { FakeStateProvider } from "../../../../spec/fake-state-provider";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { Utils } from "../../../platform/misc/utils";
@@ -22,7 +22,7 @@ import { FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state";
describe("Folder Service", () => {
let folderService: FolderService;
let cryptoService: MockProxy<CryptoService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
let i18nService: MockProxy<I18nService>;
let cipherService: MockProxy<CipherService>;
@@ -33,7 +33,7 @@ describe("Folder Service", () => {
let folderState: FakeActiveUserState<Record<string, FolderData>>;
beforeEach(() => {
cryptoService = mock<CryptoService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
i18nService = mock<I18nService>();
cipherService = mock<CipherService>();
@@ -43,14 +43,14 @@ describe("Folder Service", () => {
i18nService.collator = new Intl.Collator("en");
cryptoService.hasUserKey.mockResolvedValue(true);
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(
keyService.hasUserKey.mockResolvedValue(true);
keyService.getUserKeyWithLegacySupport.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(32)) as UserKey,
);
encryptService.decryptToUtf8.mockResolvedValue("DEC");
folderService = new FolderService(
cryptoService,
keyService,
encryptService,
i18nService,
cipherService,

View File

@@ -2,7 +2,7 @@ import { Observable, firstValueFrom, map, shareReplay } from "rxjs";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -26,7 +26,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
private decryptedFoldersState: DerivedState<FolderView[]>;
constructor(
private cryptoService: CryptoService,
private keyService: KeyService,
private encryptService: EncryptService,
private i18nService: I18nService,
private cipherService: CipherService,
@@ -36,7 +36,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
this.decryptedFoldersState = this.stateProvider.getDerived(
this.encryptedFoldersState.state$,
FOLDER_DECRYPTED_FOLDERS,
{ folderService: this, cryptoService: this.cryptoService },
{ folderService: this, keyService: this.keyService },
);
this.folders$ = this.encryptedFoldersState.state$.pipe(

View File

@@ -1,6 +1,6 @@
import { mock } from "jest-mock-extended";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { FolderService } from "../../abstractions/folder/folder.service.abstraction";
import { FolderData } from "../../models/data/folder.data";
import { Folder } from "../../models/domain/folder";
@@ -31,7 +31,7 @@ describe("encrypted folders", () => {
});
describe("derived decrypted folders", () => {
const cryptoService = mock<CryptoService>();
const keyService = mock<KeyService>();
const folderService = mock<FolderService>();
const sut = FOLDER_DECRYPTED_FOLDERS;
let data: FolderData;
@@ -64,13 +64,13 @@ describe("derived decrypted folders", () => {
it("should derive encrypted folders", async () => {
const folderViewMock = new FolderView(new Folder(data));
cryptoService.hasUserKey.mockResolvedValue(true);
keyService.hasUserKey.mockResolvedValue(true);
folderService.decryptFolders.mockResolvedValue([folderViewMock]);
const encryptedFoldersState = { id: data };
const derivedStateResult = await sut.derive(encryptedFoldersState, {
folderService,
cryptoService,
keyService,
});
expect(derivedStateResult).toEqual([folderViewMock]);

View File

@@ -1,6 +1,6 @@
import { Jsonify } from "type-fest";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { DeriveDefinition, FOLDER_DISK, UserKeyDefinition } from "../../../platform/state";
import { FolderService } from "../../abstractions/folder/folder.service.abstraction";
import { FolderData } from "../../models/data/folder.data";
@@ -19,13 +19,13 @@ export const FOLDER_ENCRYPTED_FOLDERS = UserKeyDefinition.record<FolderData>(
export const FOLDER_DECRYPTED_FOLDERS = DeriveDefinition.from<
Record<string, FolderData>,
FolderView[],
{ folderService: FolderService; cryptoService: CryptoService }
{ folderService: FolderService; keyService: KeyService }
>(FOLDER_ENCRYPTED_FOLDERS, {
deserializer: (obj) => obj.map((f) => FolderView.fromJSON(f)),
derive: async (from, { folderService, cryptoService }) => {
derive: async (from, { folderService, keyService }) => {
const folders = Object.values(from || {}).map((f) => new Folder(f));
if (await cryptoService.hasUserKey()) {
if (await keyService.hasUserKey()) {
return await folderService.decryptFolders(folders);
} else {
return [];