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:
@@ -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.
|
||||
*/
|
||||
@@ -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,
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
@@ -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> {
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 })),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("LoginUri", () => {
|
||||
encryptService = mock();
|
||||
global.bitwardenContainerService = {
|
||||
getEncryptService: () => encryptService,
|
||||
getCryptoService: () => null,
|
||||
getKeyService: () => null,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
Reference in New Issue
Block a user