mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +00:00
[PM-21033/PM-22863] User Encryption v2 (#14942)
* Add new encrypt service functions * Undo changes * Cleanup * Fix build * Fix comments * Switch encrypt service to use SDK functions * Move remaining functions to PureCrypto * Tests * Increase test coverage * Split up userkey rotation v2 and add tests * Fix eslint * Fix type errors * Fix tests * Implement signing keys * Fix sdk init * Remove key rotation v2 flag * Fix parsing when user does not have signing keys * Clear up trusted key naming * Split up getNewAccountKeys * Add trim and lowercase * Replace user.email with masterKeySalt * Add wasTrustDenied to verifyTrust in key rotation service * Move testable userkey rotation service code to testable class * Fix build * Add comments * Undo changes * Fix incorrect behavior on aborting key rotation and fix import * Fix tests * Make members of userkey rotation service protected * Fix type error * Cleanup and add injectable annotation * Fix tests * Update apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Remove v1 rotation request * Add upgrade to user encryption v2 * Fix types * Update sdk method calls * Update request models for new server api for rotation * Fix build * Update userkey rotation for new server API * Update crypto client call for new sdk changes * Fix rotation with signing keys * Cargo lock * Fix userkey rotation service * Fix types * Undo changes to feature flag service * Fix linting * [PM-22863] Account security state (#15309) * Add account security state * Update key rotation * Rename * Fix build * Cleanup * Further cleanup * Tests * Increase test coverage * Add test * Increase test coverage * Fix builds and update sdk * Fix build * Fix tests * Reset changes to encrypt service * Cleanup * Add comment * Cleanup * Cleanup * Rename model * Cleanup * Fix build * Clean up * Fix types * Cleanup * Cleanup * Cleanup * Add test * Simplify request model * Rename and add comments * Fix tests * Update responses to use less strict typing * Fix response parsing for v1 users * Update libs/common/src/key-management/keys/response/private-keys.response.ts Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update libs/common/src/key-management/keys/response/private-keys.response.ts Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Fix build * Fix build * Fix build * Undo change * Fix attachments not encrypting for v2 users --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
} from "../../../platform/models/domain/decrypt-parameters";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../../types/csprng";
|
||||
import { UnsignedPublicKey } from "../../types";
|
||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||
|
||||
export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
@@ -309,7 +310,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
"encrypt",
|
||||
]);
|
||||
const buffer = await this.subtle.exportKey("spki", impPublicKey);
|
||||
return new Uint8Array(buffer);
|
||||
return new Uint8Array(buffer) as UnsignedPublicKey;
|
||||
}
|
||||
|
||||
async aesGenerateKey(bitLength = 128 | 192 | 256 | 512): Promise<CsprngArray> {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const SigningKeyTypes = {
|
||||
Ed25519: "ed25519",
|
||||
} as const;
|
||||
|
||||
export type SigningKeyType = (typeof SigningKeyTypes)[keyof typeof SigningKeyTypes];
|
||||
export function parseSigningKeyTypeFromString(value: string): SigningKeyType {
|
||||
switch (value) {
|
||||
case SigningKeyTypes.Ed25519:
|
||||
return SigningKeyTypes.Ed25519;
|
||||
default:
|
||||
throw new Error(`Unknown signing key type: ${value}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { SecurityStateResponse } from "../../security-state/response/security-state.response";
|
||||
|
||||
import { PublicKeyEncryptionKeyPairResponse } from "./public-key-encryption-key-pair.response";
|
||||
import { SignatureKeyPairResponse } from "./signature-key-pair.response";
|
||||
|
||||
/**
|
||||
* The privately accessible view of an entity (account / org)'s keys.
|
||||
* This includes the full key-pairs for public-key encryption and signing, as well as the security state if available.
|
||||
*/
|
||||
export class PrivateKeysResponseModel {
|
||||
readonly publicKeyEncryptionKeyPair: PublicKeyEncryptionKeyPairResponse;
|
||||
readonly signatureKeyPair: SignatureKeyPairResponse | null = null;
|
||||
readonly securityState: SecurityStateResponse | null = null;
|
||||
|
||||
constructor(response: unknown) {
|
||||
if (typeof response !== "object" || response == null) {
|
||||
throw new TypeError("Response must be an object");
|
||||
}
|
||||
|
||||
if (
|
||||
!("publicKeyEncryptionKeyPair" in response) ||
|
||||
typeof response.publicKeyEncryptionKeyPair !== "object"
|
||||
) {
|
||||
throw new TypeError("Response must contain a valid publicKeyEncryptionKeyPair");
|
||||
}
|
||||
this.publicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairResponse(
|
||||
response.publicKeyEncryptionKeyPair,
|
||||
);
|
||||
|
||||
if (
|
||||
"signatureKeyPair" in response &&
|
||||
typeof response.signatureKeyPair === "object" &&
|
||||
response.signatureKeyPair != null
|
||||
) {
|
||||
this.signatureKeyPair = new SignatureKeyPairResponse(response.signatureKeyPair);
|
||||
}
|
||||
|
||||
if (
|
||||
"securityState" in response &&
|
||||
typeof response.securityState === "object" &&
|
||||
response.securityState != null
|
||||
) {
|
||||
this.securityState = new SecurityStateResponse(response.securityState);
|
||||
}
|
||||
|
||||
if (
|
||||
(this.signatureKeyPair !== null && this.securityState === null) ||
|
||||
(this.signatureKeyPair === null && this.securityState !== null)
|
||||
) {
|
||||
throw new TypeError(
|
||||
"Both signatureKeyPair and securityState must be present or absent together",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { SignedPublicKey, UnsignedPublicKey, WrappedPrivateKey } from "../../types";
|
||||
|
||||
export class PublicKeyEncryptionKeyPairResponse {
|
||||
readonly wrappedPrivateKey: WrappedPrivateKey;
|
||||
readonly publicKey: UnsignedPublicKey;
|
||||
|
||||
readonly signedPublicKey: SignedPublicKey | null = null;
|
||||
|
||||
constructor(response: unknown) {
|
||||
if (typeof response !== "object" || response == null) {
|
||||
throw new TypeError("Response must be an object");
|
||||
}
|
||||
|
||||
if (!("publicKey" in response) || typeof response.publicKey !== "string") {
|
||||
throw new TypeError("Response must contain a valid publicKey");
|
||||
}
|
||||
this.publicKey = Utils.fromB64ToArray(response.publicKey) as UnsignedPublicKey;
|
||||
|
||||
if (!("wrappedPrivateKey" in response) || typeof response.wrappedPrivateKey !== "string") {
|
||||
throw new TypeError("Response must contain a valid wrappedPrivateKey");
|
||||
}
|
||||
this.wrappedPrivateKey = response.wrappedPrivateKey as WrappedPrivateKey;
|
||||
|
||||
if ("signedPublicKey" in response && typeof response.signedPublicKey === "string") {
|
||||
this.signedPublicKey = response.signedPublicKey as SignedPublicKey;
|
||||
} else {
|
||||
this.signedPublicKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { SignedPublicKey } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { UnsignedPublicKey, VerifyingKey } from "../../types";
|
||||
|
||||
/**
|
||||
* The publicly accessible view of an entity (account / org)'s keys. That includes the encryption public key, and the verifying key if available.
|
||||
*/
|
||||
export class PublicKeysResponseModel {
|
||||
readonly publicKey: UnsignedPublicKey;
|
||||
readonly verifyingKey: VerifyingKey | null;
|
||||
readonly signedPublicKey?: SignedPublicKey | null;
|
||||
|
||||
constructor(response: unknown) {
|
||||
if (typeof response !== "object" || response == null) {
|
||||
throw new TypeError("Response must be an object");
|
||||
}
|
||||
|
||||
if (!("publicKey" in response) || !(response.publicKey instanceof Uint8Array)) {
|
||||
throw new TypeError("Response must contain a valid publicKey");
|
||||
}
|
||||
this.publicKey = response.publicKey as UnsignedPublicKey;
|
||||
|
||||
if ("verifyingKey" in response && typeof response.verifyingKey === "string") {
|
||||
this.verifyingKey = response.verifyingKey as VerifyingKey;
|
||||
} else {
|
||||
this.verifyingKey = null;
|
||||
}
|
||||
|
||||
if ("signedPublicKey" in response && typeof response.signedPublicKey === "string") {
|
||||
this.signedPublicKey = response.signedPublicKey as SignedPublicKey;
|
||||
} else {
|
||||
this.signedPublicKey = null;
|
||||
}
|
||||
|
||||
if (
|
||||
(this.signedPublicKey !== null && this.verifyingKey === null) ||
|
||||
(this.signedPublicKey === null && this.verifyingKey !== null)
|
||||
) {
|
||||
throw new TypeError(
|
||||
"Both signedPublicKey and verifyingKey must be present or absent together",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { VerifyingKey, WrappedSigningKey } from "../../types";
|
||||
|
||||
export class SignatureKeyPairResponse {
|
||||
readonly wrappedSigningKey: WrappedSigningKey;
|
||||
readonly verifyingKey: VerifyingKey;
|
||||
|
||||
constructor(response: unknown) {
|
||||
if (typeof response !== "object" || response == null) {
|
||||
throw new TypeError("Response must be an object");
|
||||
}
|
||||
|
||||
if (!("wrappedSigningKey" in response) || typeof response.wrappedSigningKey !== "string") {
|
||||
throw new TypeError("Response must contain a valid wrappedSigningKey");
|
||||
}
|
||||
this.wrappedSigningKey = response.wrappedSigningKey as WrappedSigningKey;
|
||||
|
||||
if (!("verifyingKey" in response) || typeof response.verifyingKey !== "string") {
|
||||
throw new TypeError("Response must contain a valid verifyingKey");
|
||||
}
|
||||
this.verifyingKey = response.verifyingKey as VerifyingKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PublicKeysResponseModel } from "../../response/public-keys.response";
|
||||
|
||||
export abstract class KeyApiService {
|
||||
abstract getUserPublicKeys(id: string): Promise<PublicKeysResponseModel>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { PublicKeysResponseModel } from "../response/public-keys.response";
|
||||
|
||||
import { KeyApiService } from "./abstractions/key-api-service.abstraction";
|
||||
|
||||
export class DefaultKeyApiService implements KeyApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getUserPublicKeys(id: UserId): Promise<PublicKeysResponseModel> {
|
||||
const response = await this.apiService.send("GET", "/users/" + id + "/keys", null, true, true);
|
||||
return new PublicKeysResponseModel(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { SignedSecurityState } from "../../types";
|
||||
|
||||
export abstract class SecurityStateService {
|
||||
/**
|
||||
* Retrieves the security state for the provided user.
|
||||
* Note: This state is not yet validated. To get a validated state, the SDK crypto client
|
||||
* 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>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SignedSecurityState } from "../../types";
|
||||
|
||||
export class SecurityStateRequest {
|
||||
constructor(
|
||||
readonly securityState: SignedSecurityState,
|
||||
readonly securityVersion: number,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { SignedSecurityState } from "../../types";
|
||||
|
||||
export class SecurityStateResponse {
|
||||
readonly securityState: SignedSecurityState | null = null;
|
||||
|
||||
constructor(response: unknown) {
|
||||
if (typeof response !== "object" || response == null) {
|
||||
throw new TypeError("Response must be an object");
|
||||
}
|
||||
|
||||
if (!("securityState" in response) || !(typeof response.securityState === "string")) {
|
||||
throw new TypeError("Response must contain a valid securityState");
|
||||
}
|
||||
this.securityState = response.securityState as SignedSecurityState;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
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) {}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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"],
|
||||
},
|
||||
);
|
||||
30
libs/common/src/key-management/types.ts
Normal file
30
libs/common/src/key-management/types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
import { EncString, SignedSecurityState as SdkSignedSecurityState } from "@bitwarden/sdk-internal";
|
||||
|
||||
/**
|
||||
* A private key, encrypted with a symmetric key.
|
||||
*/
|
||||
export type WrappedPrivateKey = Opaque<EncString, "WrappedPrivateKey">;
|
||||
|
||||
/**
|
||||
* A public key, signed with the accounts signature key.
|
||||
*/
|
||||
export type SignedPublicKey = Opaque<string, "SignedPublicKey">;
|
||||
/**
|
||||
* A public key in base64 encoded SPKI-DER
|
||||
*/
|
||||
export type UnsignedPublicKey = Opaque<Uint8Array, "UnsignedPublicKey">;
|
||||
|
||||
/**
|
||||
* A signature key encrypted with a symmetric key.
|
||||
*/
|
||||
export type WrappedSigningKey = Opaque<EncString, "WrappedSigningKey">;
|
||||
/**
|
||||
* A signature public key (verifying key) in base64 encoded CoseKey format
|
||||
*/
|
||||
export type VerifyingKey = Opaque<string, "VerifyingKey">;
|
||||
/**
|
||||
* A signed security state, encoded in base64.
|
||||
*/
|
||||
export type SignedSecurityState = Opaque<SdkSignedSecurityState, "SignedSecurityState">;
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PrivateKeysResponseModel } from "@bitwarden/common/key-management/keys/response/private-keys.response";
|
||||
|
||||
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";
|
||||
@@ -18,7 +20,10 @@ export class ProfileResponse extends BaseResponse {
|
||||
key?: EncString;
|
||||
avatarColor: string;
|
||||
creationDate: string;
|
||||
// Cleanup: Can be removed after moving to accountKeys
|
||||
privateKey: string;
|
||||
// Cleanup: This should be non-optional after the server has been released for a while https://bitwarden.atlassian.net/browse/PM-21768
|
||||
accountKeys: PrivateKeysResponseModel | null = null;
|
||||
securityStamp: string;
|
||||
forcePasswordReset: boolean;
|
||||
usesKeyConnector: boolean;
|
||||
@@ -37,10 +42,16 @@ export class ProfileResponse extends BaseResponse {
|
||||
this.premiumFromOrganization = this.getResponseProperty("PremiumFromOrganization");
|
||||
this.culture = this.getResponseProperty("Culture");
|
||||
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
||||
|
||||
const key = this.getResponseProperty("Key");
|
||||
if (key) {
|
||||
this.key = new EncString(key);
|
||||
}
|
||||
// Cleanup: This should be non-optional after the server has been released for a while https://bitwarden.atlassian.net/browse/PM-21768
|
||||
if (this.getResponseProperty("AccountKeys") != null) {
|
||||
this.accountKeys = new PrivateKeysResponseModel(this.getResponseProperty("AccountKeys"));
|
||||
}
|
||||
|
||||
this.avatarColor = this.getResponseProperty("AvatarColor");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.privateKey = this.getResponseProperty("PrivateKey");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { 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";
|
||||
@@ -25,3 +26,12 @@ 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"],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
// 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, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
@@ -18,6 +18,7 @@ import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey } from "../../../types/key";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
|
||||
@@ -43,6 +44,7 @@ describe("DefaultSdkService", () => {
|
||||
let platformUtilsService!: MockProxy<PlatformUtilsService>;
|
||||
let kdfConfigService!: MockProxy<KdfConfigService>;
|
||||
let keyService!: MockProxy<KeyService>;
|
||||
let securityStateService!: MockProxy<SecurityStateService>;
|
||||
let configService!: MockProxy<ConfigService>;
|
||||
let service!: DefaultSdkService;
|
||||
let accountService!: FakeAccountService;
|
||||
@@ -57,6 +59,7 @@ describe("DefaultSdkService", () => {
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
keyService = mock<KeyService>();
|
||||
securityStateService = mock<SecurityStateService>();
|
||||
apiService = mock<ApiService>();
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
@@ -75,6 +78,7 @@ describe("DefaultSdkService", () => {
|
||||
accountService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
securityStateService,
|
||||
apiService,
|
||||
fakeStateProvider,
|
||||
configService,
|
||||
@@ -100,6 +104,8 @@ describe("DefaultSdkService", () => {
|
||||
.calledWith(userId)
|
||||
.mockReturnValue(of("private-key" as EncryptedString));
|
||||
keyService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({}));
|
||||
keyService.userSigningKey$.calledWith(userId).mockReturnValue(of(null));
|
||||
securityStateService.accountSecurityState$.calledWith(userId).mockReturnValue(of(null));
|
||||
});
|
||||
|
||||
describe("given no client override has been set for the user", () => {
|
||||
|
||||
@@ -31,6 +31,8 @@ import { ApiService } from "../../../abstractions/api.service";
|
||||
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { DeviceType } from "../../../enums/device-type.enum";
|
||||
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { SecurityStateService } from "../../../key-management/security-state/abstractions/security-state.service";
|
||||
import { SignedSecurityState, WrappedSigningKey } from "../../../key-management/types";
|
||||
import { OrganizationId, UserId } from "../../../types/guid";
|
||||
import { UserKey } from "../../../types/key";
|
||||
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
|
||||
@@ -98,6 +100,7 @@ export class DefaultSdkService implements SdkService {
|
||||
private accountService: AccountService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private keyService: KeyService,
|
||||
private securityStateService: SecurityStateService,
|
||||
private apiService: ApiService,
|
||||
private stateProvider: StateProvider,
|
||||
private configService: ConfigService,
|
||||
@@ -160,10 +163,14 @@ export class DefaultSdkService implements SdkService {
|
||||
const privateKey$ = this.keyService
|
||||
.userEncryptedPrivateKey$(userId)
|
||||
.pipe(distinctUntilChanged());
|
||||
const signingKey$ = this.keyService.userSigningKey$(userId).pipe(distinctUntilChanged());
|
||||
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
|
||||
);
|
||||
const securityState$ = this.securityStateService
|
||||
.accountSecurityState$(userId)
|
||||
.pipe(distinctUntilChanged(compareValues));
|
||||
|
||||
const client$ = combineLatest([
|
||||
this.environmentService.getEnvironment$(userId),
|
||||
@@ -171,51 +178,57 @@ export class DefaultSdkService implements SdkService {
|
||||
kdfParams$,
|
||||
privateKey$,
|
||||
userKey$,
|
||||
signingKey$,
|
||||
orgKeys$,
|
||||
securityState$,
|
||||
SdkLoadService.Ready, // Makes sure we wait (once) for the SDK to be loaded
|
||||
]).pipe(
|
||||
// switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value.
|
||||
switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => {
|
||||
// Create our own observable to be able to implement clean-up logic
|
||||
return new Observable<Rc<BitwardenClient>>((subscriber) => {
|
||||
const createAndInitializeClient = async () => {
|
||||
if (env == null || kdfParams == null || privateKey == null || userKey == null) {
|
||||
return undefined;
|
||||
}
|
||||
switchMap(
|
||||
([env, account, kdfParams, privateKey, userKey, signingKey, orgKeys, securityState]) => {
|
||||
// Create our own observable to be able to implement clean-up logic
|
||||
return new Observable<Rc<BitwardenClient>>((subscriber) => {
|
||||
const createAndInitializeClient = async () => {
|
||||
if (env == null || kdfParams == null || privateKey == null || userKey == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const settings = this.toSettings(env);
|
||||
const client = await this.sdkClientFactory.createSdkClient(
|
||||
new JsTokenProvider(this.apiService, userId),
|
||||
settings,
|
||||
);
|
||||
const settings = this.toSettings(env);
|
||||
const client = await this.sdkClientFactory.createSdkClient(
|
||||
new JsTokenProvider(this.apiService, userId),
|
||||
settings,
|
||||
);
|
||||
|
||||
await this.initializeClient(
|
||||
userId,
|
||||
client,
|
||||
account,
|
||||
kdfParams,
|
||||
privateKey,
|
||||
userKey,
|
||||
orgKeys,
|
||||
);
|
||||
await this.initializeClient(
|
||||
userId,
|
||||
client,
|
||||
account,
|
||||
kdfParams,
|
||||
privateKey,
|
||||
userKey,
|
||||
signingKey,
|
||||
securityState,
|
||||
orgKeys,
|
||||
);
|
||||
|
||||
return client;
|
||||
};
|
||||
return client;
|
||||
};
|
||||
|
||||
let client: Rc<BitwardenClient> | undefined;
|
||||
createAndInitializeClient()
|
||||
.then((c) => {
|
||||
client = c === undefined ? undefined : new Rc(c);
|
||||
let client: Rc<BitwardenClient> | undefined;
|
||||
createAndInitializeClient()
|
||||
.then((c) => {
|
||||
client = c === undefined ? undefined : new Rc(c);
|
||||
|
||||
subscriber.next(client);
|
||||
})
|
||||
.catch((e) => {
|
||||
subscriber.error(e);
|
||||
});
|
||||
subscriber.next(client);
|
||||
})
|
||||
.catch((e) => {
|
||||
subscriber.error(e);
|
||||
});
|
||||
|
||||
return () => client?.markForDisposal();
|
||||
});
|
||||
}),
|
||||
return () => client?.markForDisposal();
|
||||
});
|
||||
},
|
||||
),
|
||||
tap({ finalize: () => this.sdkClientCache.delete(userId) }),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
@@ -231,6 +244,8 @@ export class DefaultSdkService implements SdkService {
|
||||
kdfParams: KdfConfig,
|
||||
privateKey: EncryptedString,
|
||||
userKey: UserKey,
|
||||
signingKey: WrappedSigningKey | null,
|
||||
securityState: SignedSecurityState | null,
|
||||
orgKeys: Record<OrganizationId, EncString>,
|
||||
) {
|
||||
await client.crypto().initialize_user_crypto({
|
||||
@@ -248,8 +263,8 @@ export class DefaultSdkService implements SdkService {
|
||||
},
|
||||
},
|
||||
privateKey,
|
||||
signingKey: undefined,
|
||||
securityState: undefined,
|
||||
signingKey: signingKey || undefined,
|
||||
securityState: securityState || undefined,
|
||||
});
|
||||
|
||||
// We initialize the org crypto even if the org_keys are
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
// 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 { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
@@ -72,6 +74,7 @@ describe("DefaultSyncService", () => {
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let stateProvider: MockProxy<StateProvider>;
|
||||
let securityStateService: MockProxy<SecurityStateService>;
|
||||
|
||||
let sut: DefaultSyncService;
|
||||
|
||||
@@ -101,6 +104,7 @@ describe("DefaultSyncService", () => {
|
||||
tokenService = mock();
|
||||
authService = mock();
|
||||
stateProvider = mock();
|
||||
securityStateService = mock();
|
||||
|
||||
sut = new DefaultSyncService(
|
||||
masterPasswordAbstraction,
|
||||
@@ -127,6 +131,7 @@ describe("DefaultSyncService", () => {
|
||||
tokenService,
|
||||
authService,
|
||||
stateProvider,
|
||||
securityStateService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -155,6 +160,142 @@ describe("DefaultSyncService", () => {
|
||||
stateProvider.getUser.mockReturnValue(mock());
|
||||
});
|
||||
|
||||
it("sets the correct keys for a V1 user with old response model", async () => {
|
||||
const v1Profile = {
|
||||
id: user1,
|
||||
key: "encryptedUserKey",
|
||||
privateKey: "privateKey",
|
||||
providers: [] as any[],
|
||||
organizations: [] as any[],
|
||||
providerOrganizations: [] as any[],
|
||||
avatarColor: "#fff",
|
||||
securityStamp: "stamp",
|
||||
emailVerified: true,
|
||||
verifyDevices: false,
|
||||
premiumPersonally: false,
|
||||
premiumFromOrganization: false,
|
||||
usesKeyConnector: false,
|
||||
};
|
||||
apiService.getSync.mockResolvedValue(
|
||||
new SyncResponse({
|
||||
profile: v1Profile,
|
||||
folders: [],
|
||||
collections: [],
|
||||
ciphers: [],
|
||||
sends: [],
|
||||
domains: [],
|
||||
policies: [],
|
||||
}),
|
||||
);
|
||||
await sut.fullSync(true);
|
||||
expect(masterPasswordAbstraction.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
new EncString("encryptedUserKey"),
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith("privateKey", user1);
|
||||
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
|
||||
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
|
||||
});
|
||||
|
||||
it("sets the correct keys for a V1 user", async () => {
|
||||
const v1Profile = {
|
||||
id: user1,
|
||||
key: "encryptedUserKey",
|
||||
privateKey: "privateKey",
|
||||
providers: [] as any[],
|
||||
organizations: [] as any[],
|
||||
providerOrganizations: [] as any[],
|
||||
avatarColor: "#fff",
|
||||
securityStamp: "stamp",
|
||||
emailVerified: true,
|
||||
verifyDevices: false,
|
||||
premiumPersonally: false,
|
||||
premiumFromOrganization: false,
|
||||
usesKeyConnector: false,
|
||||
accountKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: "wrappedPrivateKey",
|
||||
publicKey: "publicKey",
|
||||
},
|
||||
},
|
||||
};
|
||||
apiService.getSync.mockResolvedValue(
|
||||
new SyncResponse({
|
||||
profile: v1Profile,
|
||||
folders: [],
|
||||
collections: [],
|
||||
ciphers: [],
|
||||
sends: [],
|
||||
domains: [],
|
||||
policies: [],
|
||||
}),
|
||||
);
|
||||
await sut.fullSync(true);
|
||||
expect(masterPasswordAbstraction.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
new EncString("encryptedUserKey"),
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith("wrappedPrivateKey", user1);
|
||||
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
|
||||
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
|
||||
});
|
||||
|
||||
it("sets the correct keys for a V2 user", async () => {
|
||||
const v2Profile = {
|
||||
id: user1,
|
||||
key: "encryptedUserKey",
|
||||
providers: [] as unknown[],
|
||||
organizations: [] as unknown[],
|
||||
providerOrganizations: [] as unknown[],
|
||||
avatarColor: "#fff",
|
||||
securityStamp: "stamp",
|
||||
emailVerified: true,
|
||||
verifyDevices: false,
|
||||
premiumPersonally: false,
|
||||
premiumFromOrganization: false,
|
||||
usesKeyConnector: false,
|
||||
privateKey: "wrappedPrivateKey",
|
||||
accountKeys: {
|
||||
publicKeyEncryptionKeyPair: {
|
||||
wrappedPrivateKey: "wrappedPrivateKey",
|
||||
publicKey: "publicKey",
|
||||
signedPublicKey: "signedPublicKey",
|
||||
},
|
||||
signatureKeyPair: {
|
||||
wrappedSigningKey: "wrappedSigningKey",
|
||||
verifyingKey: "verifyingKey",
|
||||
},
|
||||
securityState: {
|
||||
securityState: "securityState",
|
||||
},
|
||||
},
|
||||
};
|
||||
apiService.getSync.mockResolvedValue(
|
||||
new SyncResponse({
|
||||
profile: v2Profile,
|
||||
folders: [],
|
||||
collections: [],
|
||||
ciphers: [],
|
||||
sends: [],
|
||||
domains: [],
|
||||
policies: [],
|
||||
}),
|
||||
);
|
||||
await sut.fullSync(true);
|
||||
expect(masterPasswordAbstraction.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
new EncString("encryptedUserKey"),
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith("wrappedPrivateKey", user1);
|
||||
expect(keyService.setUserSigningKey).toHaveBeenCalledWith("wrappedSigningKey", user1);
|
||||
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
|
||||
"securityState",
|
||||
user1,
|
||||
);
|
||||
expect(keyService.setProviderKeys).toHaveBeenCalledWith([], user1);
|
||||
expect(keyService.setOrgKeys).toHaveBeenCalledWith([], [], user1);
|
||||
});
|
||||
|
||||
it("does a token refresh when option missing from options", async () => {
|
||||
await sut.fullSync(true, { allowThrowOnError: false });
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CollectionService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -98,6 +99,7 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
tokenService: TokenService,
|
||||
authService: AuthService,
|
||||
stateProvider: StateProvider,
|
||||
private securityStateService: SecurityStateService,
|
||||
) {
|
||||
super(
|
||||
tokenService,
|
||||
@@ -233,13 +235,34 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
if (response?.key) {
|
||||
await this.masterPasswordService.setMasterKeyEncryptedUserKey(response.key, response.id);
|
||||
}
|
||||
await this.keyService.setPrivateKey(response.privateKey, response.id);
|
||||
|
||||
// Cleanup: Only the first branch should be kept after the server always returns accountKeys https://bitwarden.atlassian.net/browse/PM-21768
|
||||
if (response.accountKeys != null) {
|
||||
await this.keyService.setPrivateKey(
|
||||
response.accountKeys.publicKeyEncryptionKeyPair.wrappedPrivateKey,
|
||||
response.id,
|
||||
);
|
||||
if (response.accountKeys.signatureKeyPair !== null) {
|
||||
// User is V2 user
|
||||
await this.keyService.setUserSigningKey(
|
||||
response.accountKeys.signatureKeyPair.wrappedSigningKey,
|
||||
response.id,
|
||||
);
|
||||
await this.securityStateService.setAccountSecurityState(
|
||||
response.accountKeys.securityState.securityState,
|
||||
response.id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
);
|
||||
|
||||
await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor);
|
||||
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
|
||||
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
import { UnsignedPublicKey } from "../key-management/types";
|
||||
import { SymmetricCryptoKey } from "../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
// symmetric keys
|
||||
@@ -15,4 +16,4 @@ export type CipherKey = Opaque<SymmetricCryptoKey, "CipherKey">;
|
||||
|
||||
// asymmetric keys
|
||||
export type UserPrivateKey = Opaque<Uint8Array, "UserPrivateKey">;
|
||||
export type UserPublicKey = Opaque<Uint8Array, "UserPublicKey">;
|
||||
export type UserPublicKey = Opaque<UnsignedPublicKey, "UserPublicKey">;
|
||||
|
||||
Reference in New Issue
Block a user