mirror of
https://github.com/bitwarden/browser
synced 2026-01-28 15:23:53 +00:00
Remove inividual user key states and migrate to account cryptographic state
This commit is contained in:
@@ -661,6 +661,10 @@ export default class MainBackground {
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.backgroundSyncService = new BackgroundSyncService(this.taskSchedulerService);
|
||||
this.backgroundSyncService.register(() => this.fullSync());
|
||||
|
||||
@@ -732,6 +736,7 @@ export default class MainBackground {
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
this.kdfConfigService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
const pinStateService = new PinStateService(this.stateProvider);
|
||||
@@ -847,10 +852,6 @@ export default class MainBackground {
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
|
||||
@@ -73,6 +73,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { PhishingDetectionSettingsServiceAbstraction } from "@bitwarden/common/dirt/services/abstractions/phishing-detection-settings.service.abstraction";
|
||||
import { PhishingDetectionSettingsService } from "@bitwarden/common/dirt/services/phishing-detection/phishing-detection-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -294,6 +295,7 @@ const safeProviders: SafeProvider[] = [
|
||||
accountService: AccountServiceAbstraction,
|
||||
stateProvider: StateProvider,
|
||||
kdfConfigService: KdfConfigService,
|
||||
accountCryptographicStateService: AccountCryptographicStateService,
|
||||
) => {
|
||||
const keyService = new DefaultKeyService(
|
||||
masterPasswordService,
|
||||
@@ -306,6 +308,7 @@ const safeProviders: SafeProvider[] = [
|
||||
accountService,
|
||||
stateProvider,
|
||||
kdfConfigService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
new ContainerService(keyService, encryptService).attachToGlobal(self);
|
||||
return keyService;
|
||||
@@ -321,6 +324,7 @@ const safeProviders: SafeProvider[] = [
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
KdfConfigService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -439,7 +439,13 @@ export class ServiceContainer {
|
||||
this.derivedStateProvider,
|
||||
);
|
||||
|
||||
this.securityStateService = new DefaultSecurityStateService(this.stateProvider);
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.securityStateService = new DefaultSecurityStateService(
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
this.environmentService = new DefaultEnvironmentService(
|
||||
this.stateProvider,
|
||||
@@ -493,6 +499,7 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
this.kdfConfigService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
const pinStateService = new PinStateService(this.stateProvider);
|
||||
@@ -635,10 +642,6 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
const sdkClientFactory = flagEnabled("sdk")
|
||||
? new DefaultSdkClientFactory()
|
||||
: new NoopSdkClientFactory();
|
||||
|
||||
@@ -338,6 +338,7 @@ const safeProviders: SafeProvider[] = [
|
||||
BiometricStateService,
|
||||
KdfConfigService,
|
||||
DesktopBiometricsService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -36,6 +37,7 @@ describe("ElectronKeyService", () => {
|
||||
const kdfConfigService = mock<KdfConfigService>();
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
const biometricService = mock<DesktopBiometricsService>();
|
||||
const accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
@@ -62,6 +64,7 @@ describe("ElectronKeyService", () => {
|
||||
biometricStateService,
|
||||
kdfConfigService,
|
||||
biometricService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -33,6 +34,7 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
private biometricStateService: BiometricStateService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
private biometricService: DesktopBiometricsService,
|
||||
accountCryptographicStateService: AccountCryptographicStateService,
|
||||
) {
|
||||
super(
|
||||
masterPasswordService,
|
||||
@@ -45,6 +47,7 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
accountService,
|
||||
stateProvider,
|
||||
kdfConfigService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,6 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
if (!keyPair[1].encryptedString) {
|
||||
throw new Error("encrypted private key not found. Could not set private key in state.");
|
||||
}
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
{
|
||||
V1: {
|
||||
|
||||
@@ -397,7 +397,6 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId);
|
||||
expect(
|
||||
accountCryptographicStateService.setAccountCryptographicState,
|
||||
).toHaveBeenCalledWith(
|
||||
@@ -640,7 +639,9 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(
|
||||
accountCryptographicStateService.setAccountCryptographicState,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set the local master key hash to state", async () => {
|
||||
|
||||
@@ -759,12 +759,13 @@ const safeProviders: SafeProvider[] = [
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
KdfConfigService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SecurityStateService,
|
||||
useClass: DefaultSecurityStateService,
|
||||
deps: [StateProvider],
|
||||
deps: [AccountCryptographicStateService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RestrictedItemTypesService,
|
||||
@@ -1692,6 +1693,7 @@ const safeProviders: SafeProvider[] = [
|
||||
SdkService,
|
||||
ApiServiceAbstraction,
|
||||
ConfigService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -278,13 +278,6 @@ describe("LoginDecryptionOptionsComponent", () => {
|
||||
const expectedUserKey = new SymmetricCryptoKey(new Uint8Array(mockUserKeyBytes));
|
||||
|
||||
// Verify keys were set
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(mockPrivateKey, mockUserId);
|
||||
expect(keyService.setSignedPublicKey).toHaveBeenCalledWith(mockSignedPublicKey, mockUserId);
|
||||
expect(keyService.setUserSigningKey).toHaveBeenCalledWith(mockSigningKey, mockUserId);
|
||||
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
|
||||
mockSecurityState,
|
||||
mockUserId,
|
||||
);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
V2: {
|
||||
|
||||
@@ -34,11 +34,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import {
|
||||
SignedPublicKey,
|
||||
SignedSecurityState,
|
||||
WrappedSigningKey,
|
||||
} from "@bitwarden/common/key-management/types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -322,23 +317,6 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
register_result.account_cryptographic_state,
|
||||
userId,
|
||||
);
|
||||
// Legacy individual states
|
||||
await this.keyService.setPrivateKey(
|
||||
register_result.account_cryptographic_state.V2.private_key,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setSignedPublicKey(
|
||||
register_result.account_cryptographic_state.V2.signed_public_key as SignedPublicKey,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserSigningKey(
|
||||
register_result.account_cryptographic_state.V2.signing_key as WrappedSigningKey,
|
||||
userId,
|
||||
);
|
||||
await this.securityStateService.setAccountSecurityState(
|
||||
register_result.account_cryptographic_state.V2.security_state as SignedSecurityState,
|
||||
userId,
|
||||
);
|
||||
|
||||
// TDE unlock
|
||||
await this.deviceTrustService.setDeviceKey(
|
||||
|
||||
@@ -180,7 +180,10 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId);
|
||||
expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: tokenResponse.privateKey } },
|
||||
mockUserId,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets keys after a successful authentication when only userKey provided in login credentials", async () => {
|
||||
@@ -207,7 +210,10 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
mockUserId,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: tokenResponse.privateKey } },
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
// trustDeviceIfRequired should be called
|
||||
expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled();
|
||||
|
||||
@@ -120,20 +120,14 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
if (response.accountKeysResponseModel) {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
@@ -38,15 +37,13 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import {
|
||||
PasswordStrengthServiceAbstraction,
|
||||
PasswordStrengthService,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
@@ -109,6 +106,12 @@ export function identityTokenResponseFactory(
|
||||
token_type: "Bearer",
|
||||
MasterPasswordPolicy: masterPasswordPolicyResponse,
|
||||
UserDecryptionOptions: userDecryptionOptions || defaultUserDecryptionOptionsServerResponse,
|
||||
AccountKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: privateKey,
|
||||
publicKey: "PUBLIC_KEY",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,19 +204,7 @@ describe("LoginStrategy", () => {
|
||||
});
|
||||
|
||||
describe("base class", () => {
|
||||
const userKeyBytesLength = 64;
|
||||
const masterKeyBytesLength = 64;
|
||||
let userKey: UserKey;
|
||||
let masterKey: MasterKey;
|
||||
|
||||
beforeEach(() => {
|
||||
userKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(userKeyBytesLength).buffer as CsprngArray,
|
||||
) as UserKey;
|
||||
masterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(masterKeyBytesLength).buffer as CsprngArray,
|
||||
) as MasterKey;
|
||||
|
||||
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
|
||||
const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
|
||||
mockVaultTimeoutAction,
|
||||
@@ -336,28 +327,6 @@ describe("LoginStrategy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("makes a new public and private key for an old account", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.privateKey = null;
|
||||
keyService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
keyService.userKey$.mockReturnValue(new BehaviorSubject<UserKey>(userKey).asObservable());
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
// User symmetric key must be set before the new RSA keypair is generated
|
||||
expect(keyService.setUserKey).toHaveBeenCalled();
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalled();
|
||||
expect(keyService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
keyService.makeKeyPair.mock.invocationCallOrder[0],
|
||||
);
|
||||
|
||||
expect(apiService.postAccountKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if userKey is CoseEncrypt0 (V2 encryption) in createKeyPairForOldAccount", async () => {
|
||||
keyService.userKey$.mockReturnValue(
|
||||
new BehaviorSubject<UserKey>({
|
||||
|
||||
@@ -265,7 +265,7 @@ export abstract class LoginStrategy {
|
||||
|
||||
await this.setMasterKey(response, userId);
|
||||
await this.setUserKey(response, userId);
|
||||
await this.setPrivateKey(response, userId);
|
||||
await this.setAccountCryptographicState(response, userId);
|
||||
|
||||
// This needs to run after the keys are set because it checks for the existence of the encrypted private key
|
||||
await this.processForceSetPasswordReason(response.forcePasswordReset, userId);
|
||||
@@ -283,7 +283,10 @@ export abstract class LoginStrategy {
|
||||
|
||||
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
|
||||
protected abstract setPrivateKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
protected abstract setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void>;
|
||||
|
||||
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
||||
// check on password logins
|
||||
|
||||
@@ -216,7 +216,10 @@ describe("PasswordLoginStrategy", () => {
|
||||
userId,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: tokenResponse.privateKey } },
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not force the user to update their master password when there are no requirements", async () => {
|
||||
|
||||
@@ -148,20 +148,14 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
if (response.accountKeysResponseModel) {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected override encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean {
|
||||
|
||||
@@ -196,13 +196,14 @@ describe("SsoLoginStrategy", () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.key = null;
|
||||
tokenResponse.privateKey = null;
|
||||
tokenResponse.accountKeysResponseModel = null;
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();
|
||||
expect(keyService.setUserKey).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets master key encrypted user key for existing SSO users", async () => {
|
||||
|
||||
@@ -335,7 +335,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
@@ -345,20 +345,6 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
|
||||
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
|
||||
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
|
||||
// and so we don't want them falling into the createKeyPairForOldAccount flow
|
||||
await this.keyService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
userId,
|
||||
);
|
||||
} else if (tokenResponse.privateKey) {
|
||||
// User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
|
||||
// This is just existing TDE users or a TDE offboarder on an untrusted device
|
||||
await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
|
||||
@@ -188,7 +188,10 @@ describe("UserApiLoginStrategy", () => {
|
||||
tokenResponse.key,
|
||||
userId,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: tokenResponse.privateKey } },
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("gets and sets the master key if Key Connector is enabled", async () => {
|
||||
|
||||
@@ -79,20 +79,14 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
if (response.accountKeysResponseModel) {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Overridden to save client ID and secret to token service
|
||||
|
||||
@@ -261,7 +261,10 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
mockPrfPrivateKey,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey, userId);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: idTokenResponse.privateKey } },
|
||||
userId,
|
||||
);
|
||||
|
||||
// Master key and private key should not be set
|
||||
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();
|
||||
|
||||
@@ -99,20 +99,14 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(
|
||||
protected override async setAccountCryptographicState(
|
||||
response: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
if (response.accountKeysResponseModel) {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
response.accountKeysResponseModel.toWrappedAccountCryptographicState(),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
|
||||
@@ -524,14 +524,6 @@ describe("KeyConnectorService", () => {
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(mockPrivateKey, mockUserId);
|
||||
expect(keyService.setUserSigningKey).toHaveBeenCalledWith(mockSigningKey, mockUserId);
|
||||
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
|
||||
mockSecurityState,
|
||||
mockUserId,
|
||||
);
|
||||
expect(keyService.setSignedPublicKey).toHaveBeenCalledWith(mockSignedPublicKey, mockUserId);
|
||||
|
||||
expect(await firstValueFrom(conversionState.state$)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -557,10 +549,6 @@ describe("KeyConnectorService", () => {
|
||||
expect(
|
||||
accountCryptographicStateService.setAccountCryptographicState,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(keyService.setUserSigningKey).not.toHaveBeenCalled();
|
||||
expect(securityStateService.setAccountSecurityState).not.toHaveBeenCalled();
|
||||
expect(keyService.setSignedPublicKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw error when account cryptographic state is not V2", async () => {
|
||||
@@ -595,10 +583,6 @@ describe("KeyConnectorService", () => {
|
||||
expect(
|
||||
accountCryptographicStateService.setAccountCryptographicState,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(keyService.setUserSigningKey).not.toHaveBeenCalled();
|
||||
expect(securityStateService.setAccountSecurityState).not.toHaveBeenCalled();
|
||||
expect(keyService.setSignedPublicKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw error when post_keys_for_key_connector_registration fails", async () => {
|
||||
@@ -625,10 +609,6 @@ describe("KeyConnectorService", () => {
|
||||
expect(
|
||||
accountCryptographicStateService.setAccountCryptographicState,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(keyService.setUserSigningKey).not.toHaveBeenCalled();
|
||||
expect(securityStateService.setAccountSecurityState).not.toHaveBeenCalled();
|
||||
expect(keyService.setSignedPublicKey).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import { KeyGenerationService } from "../../crypto";
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
|
||||
import { SecurityStateService } from "../../security-state/abstractions/security-state.service";
|
||||
import { SignedPublicKey, SignedSecurityState, WrappedSigningKey } from "../../types";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
||||
import { KeyConnectorDomainConfirmation } from "../models/key-connector-domain-confirmation";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";
|
||||
@@ -246,22 +245,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
result.account_cryptographic_state,
|
||||
userId,
|
||||
);
|
||||
// Legacy states
|
||||
await this.keyService.setPrivateKey(result.account_cryptographic_state.V2.private_key, userId);
|
||||
await this.keyService.setUserSigningKey(
|
||||
result.account_cryptographic_state.V2.signing_key as WrappedSigningKey,
|
||||
userId,
|
||||
);
|
||||
await this.securityStateService.setAccountSecurityState(
|
||||
result.account_cryptographic_state.V2.security_state as SignedSecurityState,
|
||||
userId,
|
||||
);
|
||||
if (result.account_cryptographic_state.V2.signed_public_key != null) {
|
||||
await this.keyService.setSignedPublicKey(
|
||||
result.account_cryptographic_state.V2.signed_public_key as SignedPublicKey,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async convertNewSsoUserToKeyConnectorV1(
|
||||
|
||||
@@ -11,11 +11,4 @@ export abstract class SecurityStateService {
|
||||
* must be used. This security state is validated on initialization of the SDK.
|
||||
*/
|
||||
abstract accountSecurityState$(userId: UserId): Observable<SignedSecurityState | null>;
|
||||
/**
|
||||
* Sets the security state for the provided user.
|
||||
*/
|
||||
abstract setAccountSecurityState(
|
||||
accountSecurityState: SignedSecurityState,
|
||||
userId: UserId,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { AccountCryptographicStateService } from "../../account-cryptography/account-cryptographic-state.service";
|
||||
import { SignedSecurityState } from "../../types";
|
||||
import { SecurityStateService } from "../abstractions/security-state.service";
|
||||
import { ACCOUNT_SECURITY_STATE } from "../state/security-state.state";
|
||||
|
||||
export class DefaultSecurityStateService implements SecurityStateService {
|
||||
constructor(protected stateProvider: StateProvider) {}
|
||||
constructor(private accountCryptographicStateService: AccountCryptographicStateService) {}
|
||||
|
||||
// Emits the provided user's security state, or null if there is no security state present for the user.
|
||||
accountSecurityState$(userId: UserId): Observable<SignedSecurityState | null> {
|
||||
return this.stateProvider.getUserState$(ACCOUNT_SECURITY_STATE, userId);
|
||||
}
|
||||
return this.accountCryptographicStateService.accountCryptographicState$(userId).pipe(
|
||||
map((cryptographicState) => {
|
||||
if (cryptographicState == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sets the security state for the provided user.
|
||||
// This is not yet validated, and is only validated upon SDK initialization.
|
||||
async setAccountSecurityState(
|
||||
accountSecurityState: SignedSecurityState,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.stateProvider.setUserState(ACCOUNT_SECURITY_STATE, accountSecurityState, userId);
|
||||
if ("V2" in cryptographicState) {
|
||||
return cryptographicState.V2.security_state as SignedSecurityState;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { CRYPTO_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { SignedSecurityState } from "../../types";
|
||||
|
||||
export const ACCOUNT_SECURITY_STATE = new UserKeyDefinition<SignedSecurityState>(
|
||||
CRYPTO_DISK,
|
||||
"accountSecurityState",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
@@ -1,13 +1,4 @@
|
||||
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { USER_ENCRYPTED_PRIVATE_KEY, USER_EVER_HAD_USER_KEY } from "./user-key.state";
|
||||
|
||||
function makeEncString(data?: string) {
|
||||
data ??= Utils.newGuid();
|
||||
return new EncString(EncryptionType.AesCbc256_HmacSha256_B64, data, "test", "test");
|
||||
}
|
||||
import { USER_EVER_HAD_USER_KEY } from "./user-key.state";
|
||||
|
||||
describe("Ever had user key", () => {
|
||||
const sut = USER_EVER_HAD_USER_KEY;
|
||||
@@ -20,17 +11,3 @@ describe("Ever had user key", () => {
|
||||
expect(result).toEqual(everHadUserKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Encrypted private key", () => {
|
||||
const sut = USER_ENCRYPTED_PRIVATE_KEY;
|
||||
|
||||
it("should deserialize encrypted private key", () => {
|
||||
const encryptedPrivateKey = makeEncString().encryptedString;
|
||||
|
||||
const result = sut.deserializer(
|
||||
JSON.parse(JSON.stringify(encryptedPrivateKey as unknown)) as unknown as EncryptedString,
|
||||
);
|
||||
|
||||
expect(result).toEqual(encryptedPrivateKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { SignedPublicKey, WrappedSigningKey } from "../../../key-management/types";
|
||||
import { UserKey } from "../../../types/key";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
|
||||
import { CRYPTO_DISK, CRYPTO_MEMORY, UserKeyDefinition } from "../../state";
|
||||
@@ -13,34 +11,7 @@ export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition<boolean>(
|
||||
},
|
||||
);
|
||||
|
||||
export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition<EncryptedString>(
|
||||
CRYPTO_DISK,
|
||||
"privateKey",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export const USER_KEY = new UserKeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", {
|
||||
deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey,
|
||||
clearOn: ["logout", "lock"],
|
||||
});
|
||||
|
||||
export const USER_KEY_ENCRYPTED_SIGNING_KEY = new UserKeyDefinition<WrappedSigningKey>(
|
||||
CRYPTO_DISK,
|
||||
"userSigningKey",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export const USER_SIGNED_PUBLIC_KEY = new UserKeyDefinition<SignedPublicKey>(
|
||||
CRYPTO_DISK,
|
||||
"userSignedPublicKey",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -199,7 +199,10 @@ describe("DefaultSyncService", () => {
|
||||
new EncString("encryptedUserKey"),
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith("privateKey", user1);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: "privateKey" } },
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
|
||||
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
|
||||
});
|
||||
@@ -242,7 +245,10 @@ describe("DefaultSyncService", () => {
|
||||
new EncString("encryptedUserKey"),
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith("wrappedPrivateKey", user1);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: "wrappedPrivateKey" } },
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
|
||||
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
|
||||
});
|
||||
@@ -293,12 +299,7 @@ describe("DefaultSyncService", () => {
|
||||
new EncString("encryptedUserKey"),
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith("wrappedPrivateKey", user1);
|
||||
expect(keyService.setUserSigningKey).toHaveBeenCalledWith("wrappedSigningKey", user1);
|
||||
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
|
||||
"securityState",
|
||||
user1,
|
||||
);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalled();
|
||||
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
|
||||
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SecurityStateService } from "@bitwarden/common/key-management/security-
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { EncString as SdkEncString } from "@bitwarden/sdk-internal";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -245,29 +246,15 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
response.accountKeys.toWrappedAccountCryptographicState(),
|
||||
response.id,
|
||||
);
|
||||
|
||||
// V1 and V2 users
|
||||
await this.keyService.setPrivateKey(
|
||||
response.accountKeys.publicKeyEncryptionKeyPair.wrappedPrivateKey,
|
||||
} else {
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
{
|
||||
V1: {
|
||||
private_key: response.privateKey as SdkEncString,
|
||||
},
|
||||
},
|
||||
response.id,
|
||||
);
|
||||
// V2 users only
|
||||
if (response.accountKeys.isV2Encryption()) {
|
||||
await this.keyService.setUserSigningKey(
|
||||
response.accountKeys.signatureKeyPair.wrappedSigningKey,
|
||||
response.id,
|
||||
);
|
||||
await this.securityStateService.setAccountSecurityState(
|
||||
response.accountKeys.securityState.securityState,
|
||||
response.id,
|
||||
);
|
||||
await this.keyService.setSignedPublicKey(
|
||||
response.accountKeys.publicKeyEncryptionKeyPair.signedPublicKey,
|
||||
response.id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await this.keyService.setPrivateKey(response.privateKey, response.id);
|
||||
}
|
||||
await this.keyService.setProviderKeys(response.providers, response.id);
|
||||
await this.keyService.setOrgKeys(
|
||||
|
||||
@@ -68,20 +68,6 @@ export abstract class KeyService {
|
||||
* @param userId The desired user
|
||||
*/
|
||||
abstract setUserKey(key: UserKey, userId: UserId): 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
|
||||
@@ -263,21 +249,6 @@ export abstract class KeyService {
|
||||
* @returns The new encrypted OrgKey | ProviderKey and the decrypted key itself
|
||||
*/
|
||||
abstract makeOrgKey<T extends OrgKey | ProviderKey>(userId: UserId): 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>;
|
||||
/**
|
||||
* Sets the user's encrypted signing key in storage
|
||||
* In contrast to the private key, the decrypted signing key
|
||||
* is not stored in memory outside of the SDK.
|
||||
* @param encryptedSigningKey An encrypted signing key
|
||||
* @param userId The user id of the user to set the signing key for
|
||||
*/
|
||||
abstract setUserSigningKey(encryptedSigningKey: WrappedSigningKey, userId: UserId): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets an observable stream of the given users decrypted private key, will emit null if the user
|
||||
@@ -429,7 +400,5 @@ export abstract class KeyService {
|
||||
*/
|
||||
abstract validateUserKey(key: UserKey, userId: UserId): Promise<boolean>;
|
||||
|
||||
abstract setSignedPublicKey(signedPublicKey: SignedPublicKey, userId: UserId): Promise<void>;
|
||||
|
||||
abstract userSignedPublicKey$(userId: UserId): Observable<SignedPublicKey | null>;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, bufferCount, firstValueFrom, lastValueFrom, of, take } from "rxjs";
|
||||
|
||||
import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
EncryptedString,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { UnsignedPublicKey, WrappedSigningKey } from "@bitwarden/common/key-management/types";
|
||||
import { UnsignedPublicKey } from "@bitwarden/common/key-management/types";
|
||||
import { VaultTimeoutStringType } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { VAULT_TIMEOUT } from "@bitwarden/common/key-management/vault-timeout/services/vault-timeout-settings.state";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -22,10 +23,8 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
|
||||
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "@bitwarden/common/platform/services/key-state/org-keys.state";
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS } from "@bitwarden/common/platform/services/key-state/provider-keys.state";
|
||||
import {
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
USER_EVER_HAD_USER_KEY,
|
||||
USER_KEY,
|
||||
USER_KEY_ENCRYPTED_SIGNING_KEY,
|
||||
} from "@bitwarden/common/platform/services/key-state/user-key.state";
|
||||
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
import {
|
||||
@@ -49,7 +48,6 @@ import {
|
||||
} from "@bitwarden/common/types/key";
|
||||
|
||||
import { KdfConfigService } from "./abstractions/kdf-config.service";
|
||||
import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service";
|
||||
import { DefaultKeyService } from "./key.service";
|
||||
import { KdfConfig } from "./models/kdf-config";
|
||||
|
||||
@@ -63,6 +61,7 @@ describe("keyService", () => {
|
||||
const logService = mock<LogService>();
|
||||
const stateService = mock<StateService>();
|
||||
const kdfConfigService = mock<KdfConfigService>();
|
||||
const accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
@@ -87,6 +86,7 @@ describe("keyService", () => {
|
||||
accountService,
|
||||
stateProvider,
|
||||
kdfConfigService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -257,70 +257,6 @@ describe("keyService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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.unwrapDecapsulationKey.mockResolvedValue(mockRandomBytes);
|
||||
});
|
||||
|
||||
it("throws if userKey is null", async () => {
|
||||
await expect(
|
||||
keyService.setUserKeys(null as unknown as UserKey, mockEncPrivateKey, mockUserId),
|
||||
).rejects.toThrow("No userKey provided.");
|
||||
});
|
||||
|
||||
it("throws if encPrivateKey is null", async () => {
|
||||
await expect(
|
||||
keyService.setUserKeys(mockUserKey, null as unknown as EncryptedString, mockUserId),
|
||||
).rejects.toThrow("No encPrivateKey provided.");
|
||||
});
|
||||
|
||||
it("throws if userId is null", async () => {
|
||||
await expect(
|
||||
keyService.setUserKeys(mockUserKey, mockEncPrivateKey, null as unknown as UserId),
|
||||
).rejects.toThrow("No userId provided.");
|
||||
});
|
||||
|
||||
it("throws if encPrivateKey cannot be decrypted with the userKey", async () => {
|
||||
encryptService.unwrapDecapsulationKey.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
keyService.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(keyService, "setUserKey");
|
||||
|
||||
await keyService.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(keyService, "setPrivateKey");
|
||||
|
||||
await keyService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId);
|
||||
|
||||
expect(setEncryptedPrivateKeySpy).toHaveBeenCalledWith(mockEncPrivateKey, mockUserId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeSendKey", () => {
|
||||
const mockRandomBytes = new Uint8Array(16) as CsprngArray;
|
||||
it("calls keyGenerationService with expected hard coded parameters", async () => {
|
||||
@@ -367,22 +303,19 @@ describe("keyService", () => {
|
||||
},
|
||||
);
|
||||
|
||||
describe.each([
|
||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
USER_KEY_ENCRYPTED_SIGNING_KEY,
|
||||
USER_KEY,
|
||||
])("key removal", (key: UserKeyDefinition<unknown>) => {
|
||||
it(`clears ${key.key} for the specified user when specified`, async () => {
|
||||
const userId = "someOtherUser" as UserId;
|
||||
await keyService.clearKeys(userId);
|
||||
describe.each([USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ENCRYPTED_PROVIDER_KEYS, USER_KEY])(
|
||||
"key removal",
|
||||
(key: UserKeyDefinition<unknown>) => {
|
||||
it(`clears ${key.key} for the specified user when specified`, async () => {
|
||||
const userId = "someOtherUser" as UserId;
|
||||
await keyService.clearKeys(userId);
|
||||
|
||||
const encryptedOrgKeyState = stateProvider.singleUser.getFake(userId, key);
|
||||
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
const encryptedOrgKeyState = stateProvider.singleUser.getFake(userId, key);
|
||||
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("userPrivateKey$", () => {
|
||||
@@ -395,9 +328,9 @@ describe("keyService", () => {
|
||||
mockEncryptedPrivateKey = makeEncString("encryptedPrivateKey").encryptedString!;
|
||||
mockUserPrivateKey = makeStaticByteArray(10, 1);
|
||||
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
|
||||
stateProvider.singleUser
|
||||
.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY)
|
||||
.nextState(mockEncryptedPrivateKey);
|
||||
accountCryptographicStateService.accountCryptographicState$.mockReturnValue(
|
||||
of({ V1: { private_key: mockEncryptedPrivateKey } }),
|
||||
);
|
||||
encryptService.unwrapDecapsulationKey.mockResolvedValue(mockUserPrivateKey);
|
||||
});
|
||||
|
||||
@@ -431,7 +364,7 @@ describe("keyService", () => {
|
||||
});
|
||||
|
||||
it("returns null if encrypted private key is not set", async () => {
|
||||
stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY).nextState(null);
|
||||
accountCryptographicStateService.accountCryptographicState$.mockReturnValue(of(null));
|
||||
|
||||
const result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
|
||||
|
||||
@@ -441,6 +374,13 @@ describe("keyService", () => {
|
||||
|
||||
it("reacts to changes in user key or encrypted private key", async () => {
|
||||
// Initial state: both set
|
||||
const accountStateSubject = new BehaviorSubject({
|
||||
V1: { private_key: mockEncryptedPrivateKey },
|
||||
});
|
||||
accountCryptographicStateService.accountCryptographicState$.mockReturnValue(
|
||||
accountStateSubject.asObservable(),
|
||||
);
|
||||
|
||||
let result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
|
||||
|
||||
expect(result).toEqual(mockUserPrivateKey);
|
||||
@@ -454,7 +394,7 @@ describe("keyService", () => {
|
||||
|
||||
// Restore user key, remove encrypted private key
|
||||
stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey);
|
||||
stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY).nextState(null);
|
||||
accountStateSubject.next(null);
|
||||
|
||||
result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
|
||||
|
||||
@@ -462,52 +402,16 @@ describe("keyService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("userSigningKey$", () => {
|
||||
it("returns the signing key when the user has a signing key set", async () => {
|
||||
const fakeSigningKey = "" as WrappedSigningKey;
|
||||
const fakeSigningKeyState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_KEY_ENCRYPTED_SIGNING_KEY,
|
||||
);
|
||||
fakeSigningKeyState.nextState(fakeSigningKey);
|
||||
|
||||
const signingKey = await firstValueFrom(keyService.userSigningKey$(mockUserId));
|
||||
|
||||
expect(signingKey).toEqual(fakeSigningKey);
|
||||
});
|
||||
|
||||
it("returns null when the user does not have a signing key set", async () => {
|
||||
const signingKey = await firstValueFrom(keyService.userSigningKey$(mockUserId));
|
||||
|
||||
expect(signingKey).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUserSigningKey", () => {
|
||||
it("throws if the signing key is null", async () => {
|
||||
await expect(keyService.setUserSigningKey(null as any, mockUserId)).rejects.toThrow(
|
||||
"No user signing key provided.",
|
||||
);
|
||||
});
|
||||
it("throws if the userId is null", async () => {
|
||||
await expect(
|
||||
keyService.setUserSigningKey("" as WrappedSigningKey, null as unknown as UserId),
|
||||
).rejects.toThrow("No userId provided.");
|
||||
});
|
||||
it("sets the signing key for the user", async () => {
|
||||
const fakeSigningKey = "" as WrappedSigningKey;
|
||||
const fakeSigningKeyState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_KEY_ENCRYPTED_SIGNING_KEY,
|
||||
);
|
||||
fakeSigningKeyState.nextState(null);
|
||||
await keyService.setUserSigningKey(fakeSigningKey, mockUserId);
|
||||
expect(fakeSigningKeyState.nextMock).toHaveBeenCalledTimes(1);
|
||||
expect(fakeSigningKeyState.nextMock).toHaveBeenCalledWith(fakeSigningKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cipherDecryptionKeys$", () => {
|
||||
let accountStateSubject: BehaviorSubject<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
accountStateSubject = new BehaviorSubject(null);
|
||||
accountCryptographicStateService.accountCryptographicState$.mockReturnValue(
|
||||
accountStateSubject.asObservable(),
|
||||
);
|
||||
});
|
||||
|
||||
function fakePrivateKeyDecryption(encryptedPrivateKey: EncString, key: SymmetricCryptoKey) {
|
||||
const output = new Uint8Array(64);
|
||||
output.set(encryptedPrivateKey.dataBytes);
|
||||
@@ -544,11 +448,9 @@ describe("keyService", () => {
|
||||
}
|
||||
|
||||
if ("encryptedPrivateKey" in keys) {
|
||||
const userEncryptedPrivateKey = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey!.encryptedString!);
|
||||
accountStateSubject.next({
|
||||
V1: { private_key: keys.encryptedPrivateKey!.encryptedString! },
|
||||
});
|
||||
}
|
||||
|
||||
if ("orgKeys" in keys) {
|
||||
@@ -1240,17 +1142,16 @@ describe("keyService", () => {
|
||||
|
||||
it("successfully initializes account with new keys", async () => {
|
||||
const keyCreationSize = 512;
|
||||
const privateKeyState = stateProvider.singleUser.getFake(
|
||||
mockUserId,
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
);
|
||||
|
||||
const result = await keyService.initAccount(mockUserId);
|
||||
|
||||
expect(keyGenerationService.createKey).toHaveBeenCalledWith(keyCreationSize);
|
||||
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId);
|
||||
expect(privateKeyState.nextMock).toHaveBeenCalledWith(mockPrivateKey.encryptedString);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
{ V1: { private_key: mockPrivateKey.encryptedString } },
|
||||
mockUserId,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
userKey: userKey,
|
||||
publicKey: mockPublicKey,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ProfileOrganizationResponse } from "@bitwarden/common/admin-console/mod
|
||||
import { ProfileProviderOrganizationResponse } from "@bitwarden/common/admin-console/models/response/profile-provider-organization.response";
|
||||
import { ProfileProviderResponse } from "@bitwarden/common/admin-console/models/response/profile-provider.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -42,11 +43,8 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
|
||||
import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "@bitwarden/common/platform/services/key-state/org-keys.state";
|
||||
import { USER_ENCRYPTED_PROVIDER_KEYS } from "@bitwarden/common/platform/services/key-state/provider-keys.state";
|
||||
import {
|
||||
USER_ENCRYPTED_PRIVATE_KEY,
|
||||
USER_EVER_HAD_USER_KEY,
|
||||
USER_KEY,
|
||||
USER_KEY_ENCRYPTED_SIGNING_KEY,
|
||||
USER_SIGNED_PUBLIC_KEY,
|
||||
} from "@bitwarden/common/platform/services/key-state/user-key.state";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
@@ -60,12 +58,12 @@ import {
|
||||
UserPrivateKey,
|
||||
UserPublicKey,
|
||||
} from "@bitwarden/common/types/key";
|
||||
import { WrappedAccountCryptographicState } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { KdfConfigService } from "./abstractions/kdf-config.service";
|
||||
import {
|
||||
CipherDecryptionKeys,
|
||||
KeyService as KeyServiceAbstraction,
|
||||
UserPrivateKeyDecryptionFailedError,
|
||||
} from "./abstractions/key.service";
|
||||
import { KdfConfig } from "./models/kdf-config";
|
||||
|
||||
@@ -90,6 +88,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
protected accountService: AccountService,
|
||||
protected stateProvider: StateProvider,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected accountCryptographyStateService: AccountCryptographicStateService,
|
||||
) {
|
||||
this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe(
|
||||
switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)),
|
||||
@@ -121,30 +120,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
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(userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
throw new Error("UserId is required.");
|
||||
@@ -479,16 +454,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
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 getFingerprint(fingerprintMaterial: string, publicKey: Uint8Array): Promise<string[]> {
|
||||
if (publicKey == null) {
|
||||
throw new Error("Public key is required to generate a fingerprint.");
|
||||
@@ -515,18 +480,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return [publicB64, privateEnc];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the user's key pair
|
||||
* @param userId The desired user
|
||||
*/
|
||||
private async clearKeyPair(userId: UserId): Promise<void> {
|
||||
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
|
||||
}
|
||||
|
||||
private async clearSigningKey(userId: UserId): Promise<void> {
|
||||
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_SIGNING_KEY, null, userId);
|
||||
}
|
||||
|
||||
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
|
||||
return await this.keyGenerationService.deriveKeyFromMaterial(
|
||||
keyMaterial,
|
||||
@@ -548,8 +501,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
await this.clearUserKey(userId);
|
||||
await this.clearOrgKeys(userId);
|
||||
await this.clearProviderKeys(userId);
|
||||
await this.clearKeyPair(userId);
|
||||
await this.clearSigningKey(userId);
|
||||
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
|
||||
}
|
||||
|
||||
@@ -595,9 +546,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
|
||||
try {
|
||||
const encPrivateKey = await firstValueFrom(
|
||||
this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$,
|
||||
);
|
||||
const encPrivateKey = await firstValueFrom(this.userEncryptedPrivateKey$(userId));
|
||||
|
||||
if (encPrivateKey == null) {
|
||||
return false;
|
||||
@@ -655,9 +604,14 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
|
||||
await this.setUserKey(userKey, userId);
|
||||
await this.stateProvider
|
||||
.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY)
|
||||
.update(() => privateKey.encryptedString!);
|
||||
await this.accountCryptographyStateService.setAccountCryptographicState(
|
||||
{
|
||||
V1: {
|
||||
private_key: privateKey.encryptedString,
|
||||
},
|
||||
},
|
||||
userId,
|
||||
);
|
||||
|
||||
return {
|
||||
userKey,
|
||||
@@ -801,7 +755,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
|
||||
userEncryptedPrivateKey$(userId: UserId): Observable<EncryptedString | null> {
|
||||
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
|
||||
return this.accountCryptographyStateService.accountCryptographicState$(userId).pipe(
|
||||
map((state: WrappedAccountCryptographicState | null) => {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
if ("V2" in state) {
|
||||
return state.V2.private_key;
|
||||
} else if ("V1" in state) {
|
||||
return state.V1.private_key;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private userPrivateKeyHelper$(userId: UserId) {
|
||||
@@ -812,7 +779,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$.pipe(
|
||||
return this.userEncryptedPrivateKey$(userId).pipe(
|
||||
switchMap(async (encryptedPrivateKey) => {
|
||||
try {
|
||||
return await this.decryptPrivateKey(encryptedPrivateKey, userKey);
|
||||
@@ -879,23 +846,17 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserSigningKey(userSigningKey: WrappedSigningKey, userId: UserId): Promise<void> {
|
||||
if (userSigningKey == null) {
|
||||
throw new Error("No user signing key provided.");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new Error("No userId provided.");
|
||||
}
|
||||
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_SIGNING_KEY, userSigningKey, userId);
|
||||
}
|
||||
|
||||
userSigningKey$(userId: UserId): Observable<WrappedSigningKey | null> {
|
||||
return this.stateProvider.getUser(userId, USER_KEY_ENCRYPTED_SIGNING_KEY).state$.pipe(
|
||||
map((encryptedSigningKey) => {
|
||||
if (encryptedSigningKey == null) {
|
||||
return this.accountCryptographyStateService.accountCryptographicState$(userId).pipe(
|
||||
map((state: WrappedAccountCryptographicState | null) => {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
if ("V2" in state) {
|
||||
return state.V2.signing_key as WrappedSigningKey;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return encryptedSigningKey as WrappedSigningKey;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1017,11 +978,18 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async setSignedPublicKey(signedPublicKey: SignedPublicKey, userId: UserId): Promise<void> {
|
||||
await this.stateProvider.setUserState(USER_SIGNED_PUBLIC_KEY, signedPublicKey, userId);
|
||||
}
|
||||
|
||||
userSignedPublicKey$(userId: UserId): Observable<SignedPublicKey | null> {
|
||||
return this.stateProvider.getUserState$(USER_SIGNED_PUBLIC_KEY, userId);
|
||||
return this.accountCryptographyStateService.accountCryptographicState$(userId).pipe(
|
||||
map((state: WrappedAccountCryptographicState | null) => {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
if ("V2" in state) {
|
||||
return state.V2.signed_public_key as SignedPublicKey;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of, throwError } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -70,6 +71,7 @@ describe("regenerateIfNeeded", () => {
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock<KeyService>();
|
||||
@@ -80,6 +82,7 @@ describe("regenerateIfNeeded", () => {
|
||||
apiService = mock<ApiService>();
|
||||
configService = mock<ConfigService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
|
||||
sut = new DefaultUserAsymmetricKeysRegenerationService(
|
||||
keyService,
|
||||
@@ -89,6 +92,7 @@ describe("regenerateIfNeeded", () => {
|
||||
sdkService,
|
||||
apiService,
|
||||
configService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
@@ -131,7 +135,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when private key is decryptable and valid", async () => {
|
||||
@@ -146,7 +150,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when user symmetric key is unavailable", async () => {
|
||||
@@ -162,7 +166,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when user's encrypted private key is unavailable", async () => {
|
||||
@@ -180,7 +184,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when user's public key is unavailable", async () => {
|
||||
@@ -196,7 +200,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should regenerate when private key is decryptable and invalid", async () => {
|
||||
@@ -211,7 +215,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not set private key on known API error", async () => {
|
||||
@@ -230,7 +234,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not set private key on unknown API error", async () => {
|
||||
@@ -249,7 +253,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should regenerate when private key is not decryptable and user key is valid", async () => {
|
||||
@@ -265,7 +269,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when private key is not decryptable and user key is invalid", async () => {
|
||||
@@ -283,7 +287,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when private key is not decryptable and no ciphers to check", async () => {
|
||||
@@ -299,7 +303,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should regenerate when private key is not decryptable and invalid and user key is valid", async () => {
|
||||
@@ -315,7 +319,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when private key is not decryptable and invalid and user key is invalid", async () => {
|
||||
@@ -333,7 +337,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when private key is not decryptable and invalid and no ciphers to check", async () => {
|
||||
@@ -349,7 +353,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not regenerate when userKey type is CoseEncrypt0 (V2 encryption)", async () => {
|
||||
@@ -364,7 +368,7 @@ describe("regenerateIfNeeded", () => {
|
||||
expect(
|
||||
userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(keyService.setPrivateKey).not.toHaveBeenCalled();
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).not.toHaveBeenCalled();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
"[UserAsymmetricKeyRegeneration] Cannot regenerate asymmetric keys for accounts on V2 encryption.",
|
||||
);
|
||||
@@ -382,6 +386,7 @@ describe("regenerateUserPublicKeyEncryptionKeyPair", () => {
|
||||
let sdkService: MockSdkService;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock<KeyService>();
|
||||
@@ -391,6 +396,7 @@ describe("regenerateUserPublicKeyEncryptionKeyPair", () => {
|
||||
sdkService = new MockSdkService();
|
||||
apiService = mock<ApiService>();
|
||||
configService = mock<ConfigService>();
|
||||
accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
|
||||
sut = new DefaultUserAsymmetricKeysRegenerationService(
|
||||
keyService,
|
||||
@@ -400,6 +406,7 @@ describe("regenerateUserPublicKeyEncryptionKeyPair", () => {
|
||||
sdkService,
|
||||
apiService,
|
||||
configService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { combineLatest, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -24,6 +25,7 @@ export class DefaultUserAsymmetricKeysRegenerationService implements UserAsymmet
|
||||
private sdkService: SdkService,
|
||||
private apiService: ApiService,
|
||||
private configService: ConfigService,
|
||||
private accountCryptographyStateService: AccountCryptographicStateService,
|
||||
) {}
|
||||
|
||||
async regenerateIfNeeded(userId: UserId): Promise<void> {
|
||||
@@ -161,7 +163,14 @@ export class DefaultUserAsymmetricKeysRegenerationService implements UserAsymmet
|
||||
return;
|
||||
}
|
||||
|
||||
await this.keyService.setPrivateKey(makeKeyPairResponse.userKeyEncryptedPrivateKey, userId);
|
||||
await this.accountCryptographyStateService.setAccountCryptographicState(
|
||||
{
|
||||
V1: {
|
||||
private_key: makeKeyPairResponse.userKeyEncryptedPrivateKey,
|
||||
},
|
||||
},
|
||||
userId,
|
||||
);
|
||||
this.logService.info(
|
||||
"[UserAsymmetricKeyRegeneration] User's asymmetric keys successfully regenerated.",
|
||||
);
|
||||
|
||||
@@ -71,12 +71,13 @@ import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-r
|
||||
import { RemoveAccountDeprovisioningBannerDismissed } from "./migrations/72-remove-account-deprovisioning-banner-dismissed";
|
||||
import { AddMasterPasswordUnlockData } from "./migrations/73-add-master-password-unlock-data";
|
||||
import { RemoveLegacyPin } from "./migrations/74-remove-legacy-pin";
|
||||
import { RemoveUserEncryptedPrivateKey } from "./migrations/75-remove-user-encrypted-private-key";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 74;
|
||||
export const CURRENT_VERSION = 75;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@@ -152,7 +153,8 @@ export function createMigrationBuilder() {
|
||||
.with(RemoveNewCustomizationOptionsCalloutDismissed, 70, 71)
|
||||
.with(RemoveAccountDeprovisioningBannerDismissed, 71, 72)
|
||||
.with(AddMasterPasswordUnlockData, 72, 73)
|
||||
.with(RemoveLegacyPin, 73, CURRENT_VERSION);
|
||||
.with(RemoveLegacyPin, 73, 74)
|
||||
.with(RemoveUserEncryptedPrivateKey, 74, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
import { IRREVERSIBLE } from "../migrator";
|
||||
|
||||
import { RemoveUserEncryptedPrivateKey } from "./75-remove-user-encrypted-private-key";
|
||||
|
||||
describe("RemoveUserEncryptedPrivateKey", () => {
|
||||
const sut = new RemoveUserEncryptedPrivateKey(74, 75);
|
||||
|
||||
describe("migrate", () => {
|
||||
it("deletes user encrypted private key, signing key, and signed public key from all users", async () => {
|
||||
const output = await runMigrator(sut, {
|
||||
global_account_accounts: {
|
||||
user1: {
|
||||
email: "user1@email.com",
|
||||
name: "User 1",
|
||||
emailVerified: true,
|
||||
},
|
||||
user2: {
|
||||
email: "user2@email.com",
|
||||
name: "User 2",
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
user_user1_CRYPTO_DISK_privateKey: "abc",
|
||||
user_user2_CRYPTO_DISK_privateKey: "def",
|
||||
user_user1_CRYPTO_DISK_userSigningKey: "sign1",
|
||||
user_user2_CRYPTO_DISK_userSigningKey: "sign2",
|
||||
user_user1_CRYPTO_DISK_userSignedPublicKey: "pub1",
|
||||
user_user2_CRYPTO_DISK_userSignedPublicKey: "pub2",
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
global_account_accounts: {
|
||||
user1: {
|
||||
email: "user1@email.com",
|
||||
name: "User 1",
|
||||
emailVerified: true,
|
||||
},
|
||||
user2: {
|
||||
email: "user2@email.com",
|
||||
name: "User 2",
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
it("is irreversible", async () => {
|
||||
await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
export const accountSecurityState: KeyDefinitionLike = {
|
||||
key: "accountSecurityState",
|
||||
stateDefinition: {
|
||||
name: "CRYPTO_DISK",
|
||||
},
|
||||
};
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { IRREVERSIBLE, Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = NonNullable<unknown>;
|
||||
|
||||
export const userEncryptedPrivateKey: KeyDefinitionLike = {
|
||||
key: "privateKey",
|
||||
stateDefinition: {
|
||||
name: "CRYPTO_DISK",
|
||||
},
|
||||
};
|
||||
|
||||
export const userKeyEncryptedSigningKey: KeyDefinitionLike = {
|
||||
key: "userSigningKey",
|
||||
stateDefinition: {
|
||||
name: "CRYPTO_DISK",
|
||||
},
|
||||
};
|
||||
|
||||
export const userSignedPublicKey: KeyDefinitionLike = {
|
||||
key: "userSignedPublicKey",
|
||||
stateDefinition: {
|
||||
name: "CRYPTO_DISK",
|
||||
},
|
||||
};
|
||||
|
||||
export class RemoveUserEncryptedPrivateKey extends Migrator<74, 75> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
// Remove privateKey
|
||||
const key = await helper.getFromUser(userId, userEncryptedPrivateKey);
|
||||
if (key != null) {
|
||||
await helper.removeFromUser(userId, userEncryptedPrivateKey);
|
||||
}
|
||||
// Remove userSigningKey
|
||||
const signingKey = await helper.getFromUser(userId, userKeyEncryptedSigningKey);
|
||||
if (signingKey != null) {
|
||||
await helper.removeFromUser(userId, userKeyEncryptedSigningKey);
|
||||
}
|
||||
// Remove userSignedPublicKey
|
||||
const signedPubKey = await helper.getFromUser(userId, userSignedPublicKey);
|
||||
if (signedPubKey != null) {
|
||||
await helper.removeFromUser(userId, userSignedPublicKey);
|
||||
}
|
||||
// Remove accountSecurityState
|
||||
const accountSecurity = await helper.getFromUser(userId, accountSecurityState);
|
||||
if (accountSecurity != null) {
|
||||
await helper.removeFromUser(userId, accountSecurityState);
|
||||
}
|
||||
}
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
throw IRREVERSIBLE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user