mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-17670] Move KeyConnectorService to KM ownership (#13277)
* Move KeyConnectorService to KM ownership * Add to codecov * Move key connector request models
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
export abstract class KeyConnectorService {
|
||||
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
|
||||
getManagingOrganization: (userId?: UserId) => Promise<Organization>;
|
||||
getUsesKeyConnector: (userId: UserId) => Promise<boolean>;
|
||||
migrateUser: (userId?: UserId) => Promise<void>;
|
||||
userNeedsMigration: (userId: UserId) => Promise<boolean>;
|
||||
convertNewSsoUserToKeyConnector: (
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
orgId: string,
|
||||
userId: UserId,
|
||||
) => Promise<void>;
|
||||
setUsesKeyConnector: (enabled: boolean, userId: UserId) => Promise<void>;
|
||||
setConvertAccountRequired: (status: boolean, userId?: UserId) => Promise<void>;
|
||||
getConvertAccountRequired: () => Promise<boolean>;
|
||||
removeConvertAccountRequired: (userId?: UserId) => Promise<void>;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export class KeyConnectorUserKeyRequest {
|
||||
key: string;
|
||||
|
||||
constructor(key: string) {
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { KdfConfig, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
import { KeysRequest } from "../../../models/request/keys.request";
|
||||
|
||||
export class SetKeyConnectorKeyRequest {
|
||||
key: string;
|
||||
keys: KeysRequest;
|
||||
kdf: KdfType;
|
||||
kdfIterations: number;
|
||||
kdfMemory?: number;
|
||||
kdfParallelism?: number;
|
||||
orgIdentifier: string;
|
||||
|
||||
constructor(key: string, kdfConfig: KdfConfig, orgIdentifier: string, keys: KeysRequest) {
|
||||
this.key = key;
|
||||
this.kdf = kdfConfig.kdfType;
|
||||
this.kdfIterations = kdfConfig.iterations;
|
||||
if (kdfConfig.kdfType === KdfType.Argon2id) {
|
||||
this.kdfMemory = kdfConfig.memory;
|
||||
this.kdfParallelism = kdfConfig.parallelism;
|
||||
}
|
||||
this.orgIdentifier = orgIdentifier;
|
||||
this.keys = keys;
|
||||
}
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { KeyGenerationService } from "../../platform/services/key-generation.service";
|
||||
import { OrganizationId, UserId } from "../../types/guid";
|
||||
import { MasterKey } from "../../types/key";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request";
|
||||
import { KeyConnectorUserKeyResponse } from "../models/response/key-connector-user-key.response";
|
||||
|
||||
import {
|
||||
USES_KEY_CONNECTOR,
|
||||
CONVERT_ACCOUNT_TO_KEY_CONNECTOR,
|
||||
KeyConnectorService,
|
||||
} from "./key-connector.service";
|
||||
import { FakeMasterPasswordService } from "./master-password/fake-master-password.service";
|
||||
import { TokenService } from "./token.service";
|
||||
|
||||
describe("KeyConnectorService", () => {
|
||||
let keyConnectorService: KeyConnectorService;
|
||||
|
||||
const keyService = mock<KeyService>();
|
||||
const apiService = mock<ApiService>();
|
||||
const tokenService = mock<TokenService>();
|
||||
const logService = mock<LogService>();
|
||||
const organizationService = mock<OrganizationService>();
|
||||
const keyGenerationService = mock<KeyGenerationService>();
|
||||
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
let accountService: FakeAccountService;
|
||||
let masterPasswordService: FakeMasterPasswordService;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const mockOrgId = Utils.newGuid() as OrganizationId;
|
||||
|
||||
const mockMasterKeyResponse: KeyConnectorUserKeyResponse = new KeyConnectorUserKeyResponse({
|
||||
key: "eO9nVlVl3I3sU6O+CyK0kEkpGtl/auT84Hig2WTXmZtDTqYtKpDvUPfjhgMOHf+KQzx++TVS2AOLYq856Caa7w==",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
masterPasswordService = new FakeMasterPasswordService();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
keyConnectorService = new KeyConnectorService(
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
keyService,
|
||||
apiService,
|
||||
tokenService,
|
||||
logService,
|
||||
organizationService,
|
||||
keyGenerationService,
|
||||
async () => {},
|
||||
stateProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(keyConnectorService).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("setUsesKeyConnector()", () => {
|
||||
it("should update the usesKeyConnectorState with the provided value", async () => {
|
||||
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
|
||||
state.nextState(false);
|
||||
|
||||
const newValue = true;
|
||||
|
||||
await keyConnectorService.setUsesKeyConnector(newValue, mockUserId);
|
||||
|
||||
expect(await keyConnectorService.getUsesKeyConnector(mockUserId)).toBe(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getManagingOrganization()", () => {
|
||||
it("should return the managing organization with key connector enabled", async () => {
|
||||
// Arrange
|
||||
const orgs = [
|
||||
organizationData(true, true, "https://key-connector-url.com", 2, false),
|
||||
organizationData(false, true, "https://key-connector-url.com", 2, false),
|
||||
organizationData(true, false, "https://key-connector-url.com", 2, false),
|
||||
organizationData(true, true, "https://other-url.com", 2, false),
|
||||
];
|
||||
organizationService.organizations$.mockReturnValue(of(orgs));
|
||||
|
||||
// Act
|
||||
const result = await keyConnectorService.getManagingOrganization();
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(orgs[0]);
|
||||
});
|
||||
|
||||
it("should return undefined if no managing organization with key connector enabled is found", async () => {
|
||||
// Arrange
|
||||
const orgs = [
|
||||
organizationData(true, false, "https://key-connector-url.com", 2, false),
|
||||
organizationData(false, false, "https://key-connector-url.com", 2, false),
|
||||
];
|
||||
organizationService.organizations$.mockReturnValue(of(orgs));
|
||||
|
||||
// Act
|
||||
const result = await keyConnectorService.getManagingOrganization();
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined if user is Owner or Admin", async () => {
|
||||
// Arrange
|
||||
const orgs = [
|
||||
organizationData(true, true, "https://key-connector-url.com", 0, false),
|
||||
organizationData(true, true, "https://key-connector-url.com", 1, false),
|
||||
];
|
||||
organizationService.organizations$.mockReturnValue(of(orgs));
|
||||
|
||||
// Act
|
||||
const result = await keyConnectorService.getManagingOrganization();
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined if user is a Provider", async () => {
|
||||
// Arrange
|
||||
const orgs = [
|
||||
organizationData(true, true, "https://key-connector-url.com", 2, true),
|
||||
organizationData(false, true, "https://key-connector-url.com", 2, true),
|
||||
];
|
||||
organizationService.organizations$.mockReturnValue(of(orgs));
|
||||
|
||||
// Act
|
||||
const result = await keyConnectorService.getManagingOrganization();
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setConvertAccountRequired()", () => {
|
||||
it("should update the convertAccountToKeyConnectorState with the provided value", async () => {
|
||||
const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR);
|
||||
state.nextState(false);
|
||||
|
||||
const newValue = true;
|
||||
|
||||
await keyConnectorService.setConvertAccountRequired(newValue);
|
||||
|
||||
expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue);
|
||||
});
|
||||
|
||||
it("should remove the convertAccountToKeyConnectorState", async () => {
|
||||
const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR);
|
||||
state.nextState(false);
|
||||
|
||||
const newValue: boolean = null;
|
||||
|
||||
await keyConnectorService.setConvertAccountRequired(newValue);
|
||||
|
||||
expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("userNeedsMigration()", () => {
|
||||
it("should return true if the user needs migration", async () => {
|
||||
// token
|
||||
tokenService.getIsExternal.mockResolvedValue(true);
|
||||
|
||||
// create organization object
|
||||
const data = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
organizationService.organizations$.mockReturnValue(of([data]));
|
||||
|
||||
// uses KeyConnector
|
||||
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
|
||||
state.nextState(false);
|
||||
|
||||
const result = await keyConnectorService.userNeedsMigration(mockUserId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the user does not need migration", async () => {
|
||||
tokenService.getIsExternal.mockResolvedValue(false);
|
||||
const data = organizationData(false, false, "https://key-connector-url.com", 2, false);
|
||||
organizationService.organizations$.mockReturnValue(of([data]));
|
||||
|
||||
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
|
||||
state.nextState(true);
|
||||
const result = await keyConnectorService.userNeedsMigration(mockUserId);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setMasterKeyFromUrl", () => {
|
||||
it("should set the master key from the provided URL", async () => {
|
||||
// Arrange
|
||||
const url = "https://key-connector-url.com";
|
||||
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse);
|
||||
|
||||
// Hard to mock these, but we can generate the same keys
|
||||
const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key);
|
||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||
|
||||
// Act
|
||||
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
||||
expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(
|
||||
masterKey,
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle errors thrown during the process", async () => {
|
||||
// Arrange
|
||||
const url = "https://key-connector-url.com";
|
||||
|
||||
const error = new Error("Failed to get master key");
|
||||
apiService.getMasterKeyFromKeyConnector.mockRejectedValue(error);
|
||||
jest.spyOn(logService, "error");
|
||||
|
||||
try {
|
||||
// Act
|
||||
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||
} catch {
|
||||
// Assert
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrateUser()", () => {
|
||||
it("should migrate the user to the key connector", async () => {
|
||||
// Arrange
|
||||
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
const masterKey = getMockMasterKey();
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
|
||||
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
|
||||
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
|
||||
|
||||
// Act
|
||||
await keyConnectorService.migrateUser();
|
||||
|
||||
// Assert
|
||||
expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled();
|
||||
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
|
||||
organization.keyConnectorUrl,
|
||||
keyConnectorRequest,
|
||||
);
|
||||
expect(apiService.postConvertToKeyConnector).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle errors thrown during migration", async () => {
|
||||
// Arrange
|
||||
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
const masterKey = getMockMasterKey();
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
const error = new Error("Failed to post user key to key connector");
|
||||
organizationService.organizations$.mockReturnValue(of([organization]));
|
||||
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
|
||||
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error);
|
||||
jest.spyOn(logService, "error");
|
||||
|
||||
try {
|
||||
// Act
|
||||
await keyConnectorService.migrateUser();
|
||||
} catch {
|
||||
// Assert
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled();
|
||||
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
|
||||
organization.keyConnectorUrl,
|
||||
keyConnectorRequest,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function organizationData(
|
||||
usesKeyConnector: boolean,
|
||||
keyConnectorEnabled: boolean,
|
||||
keyConnectorUrl: string,
|
||||
userType: number,
|
||||
isProviderUser: boolean,
|
||||
): Organization {
|
||||
return new Organization(
|
||||
new OrganizationData(
|
||||
new ProfileOrganizationResponse({
|
||||
id: mockOrgId,
|
||||
name: "TEST_KEY_CONNECTOR_ORG",
|
||||
usePolicies: true,
|
||||
useSso: true,
|
||||
useKeyConnector: usesKeyConnector,
|
||||
useScim: true,
|
||||
useGroups: true,
|
||||
useDirectory: true,
|
||||
useEvents: true,
|
||||
useTotp: true,
|
||||
use2fa: true,
|
||||
useApi: true,
|
||||
useResetPassword: true,
|
||||
useSecretsManager: true,
|
||||
usePasswordManager: true,
|
||||
usersGetPremium: true,
|
||||
useCustomPermissions: true,
|
||||
useActivateAutofillPolicy: true,
|
||||
selfHost: true,
|
||||
seats: 5,
|
||||
maxCollections: null,
|
||||
maxStorageGb: 1,
|
||||
key: "super-secret-key",
|
||||
status: 2,
|
||||
type: userType,
|
||||
enabled: true,
|
||||
ssoBound: true,
|
||||
identifier: "TEST_KEY_CONNECTOR_ORG",
|
||||
permissions: {
|
||||
accessEventLogs: false,
|
||||
accessImportExport: false,
|
||||
accessReports: false,
|
||||
createNewCollections: false,
|
||||
editAnyCollection: false,
|
||||
deleteAnyCollection: false,
|
||||
manageGroups: false,
|
||||
managePolicies: false,
|
||||
manageSso: false,
|
||||
manageUsers: false,
|
||||
manageResetPassword: false,
|
||||
manageScim: false,
|
||||
},
|
||||
resetPasswordEnrolled: true,
|
||||
userId: mockUserId,
|
||||
hasPublicAndPrivateKeys: true,
|
||||
providerId: null,
|
||||
providerName: null,
|
||||
providerType: null,
|
||||
familySponsorshipFriendlyName: null,
|
||||
familySponsorshipAvailable: true,
|
||||
planProductType: 3,
|
||||
KeyConnectorEnabled: keyConnectorEnabled,
|
||||
KeyConnectorUrl: keyConnectorUrl,
|
||||
familySponsorshipLastSyncDate: null,
|
||||
familySponsorshipValidUntil: null,
|
||||
familySponsorshipToDelete: null,
|
||||
accessSecretsManager: false,
|
||||
limitCollectionCreation: true,
|
||||
limitCollectionDeletion: true,
|
||||
limitItemDeletion: true,
|
||||
allowAdminAccessToAllCollectionItems: true,
|
||||
flexibleCollections: false,
|
||||
object: "profileOrganization",
|
||||
}),
|
||||
{ isMember: true, isProviderUser: isProviderUser },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function getMockMasterKey(): MasterKey {
|
||||
const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key);
|
||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||
return masterKey;
|
||||
}
|
||||
});
|
||||
@@ -1,208 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
PBKDF2KdfConfig,
|
||||
KeyService,
|
||||
KdfType,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationUserType } from "../../admin-console/enums";
|
||||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { KeysRequest } from "../../models/request/keys.request";
|
||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
ActiveUserState,
|
||||
KEY_CONNECTOR_DISK,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { MasterKey } from "../../types/key";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
export const USES_KEY_CONNECTOR = new UserKeyDefinition<boolean | null>(
|
||||
KEY_CONNECTOR_DISK,
|
||||
"usesKeyConnector",
|
||||
{
|
||||
deserializer: (usesKeyConnector) => usesKeyConnector,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export const CONVERT_ACCOUNT_TO_KEY_CONNECTOR = new UserKeyDefinition<boolean | null>(
|
||||
KEY_CONNECTOR_DISK,
|
||||
"convertAccountToKeyConnector",
|
||||
{
|
||||
deserializer: (convertAccountToKeyConnector) => convertAccountToKeyConnector,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
private usesKeyConnectorState: ActiveUserState<boolean>;
|
||||
private convertAccountToKeyConnectorState: ActiveUserState<boolean>;
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
private keyService: KeyService,
|
||||
private apiService: ApiService,
|
||||
private tokenService: TokenService,
|
||||
private logService: LogService,
|
||||
private organizationService: OrganizationService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private logoutCallback: (logoutReason: LogoutReason, userId?: string) => Promise<void>,
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.usesKeyConnectorState = this.stateProvider.getActive(USES_KEY_CONNECTOR);
|
||||
this.convertAccountToKeyConnectorState = this.stateProvider.getActive(
|
||||
CONVERT_ACCOUNT_TO_KEY_CONNECTOR,
|
||||
);
|
||||
}
|
||||
|
||||
async setUsesKeyConnector(usesKeyConnector: boolean, userId: UserId) {
|
||||
await this.stateProvider.getUser(userId, USES_KEY_CONNECTOR).update(() => usesKeyConnector);
|
||||
}
|
||||
|
||||
getUsesKeyConnector(userId: UserId): Promise<boolean> {
|
||||
return firstValueFrom(this.stateProvider.getUserState$(USES_KEY_CONNECTOR, userId));
|
||||
}
|
||||
|
||||
async userNeedsMigration(userId: UserId) {
|
||||
const loggedInUsingSso = await this.tokenService.getIsExternal(userId);
|
||||
const requiredByOrganization = (await this.getManagingOrganization(userId)) != null;
|
||||
const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector(userId));
|
||||
|
||||
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
|
||||
}
|
||||
|
||||
async migrateUser(userId?: UserId) {
|
||||
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const organization = await this.getManagingOrganization(userId);
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
|
||||
try {
|
||||
await this.apiService.postUserKeyToKeyConnector(
|
||||
organization.keyConnectorUrl,
|
||||
keyConnectorRequest,
|
||||
);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
|
||||
await this.apiService.postConvertToKeyConnector();
|
||||
}
|
||||
|
||||
// TODO: UserKey should be renamed to MasterKey and typed accordingly
|
||||
async setMasterKeyFromUrl(url: string, userId: UserId) {
|
||||
try {
|
||||
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
|
||||
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
|
||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async getManagingOrganization(userId?: UserId): Promise<Organization> {
|
||||
const orgs = await firstValueFrom(this.organizationService.organizations$(userId));
|
||||
return orgs.find(
|
||||
(o) =>
|
||||
o.keyConnectorEnabled &&
|
||||
o.type !== OrganizationUserType.Admin &&
|
||||
o.type !== OrganizationUserType.Owner &&
|
||||
!o.isProviderUser,
|
||||
);
|
||||
}
|
||||
|
||||
async convertNewSsoUserToKeyConnector(
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
orgId: string,
|
||||
userId: UserId,
|
||||
) {
|
||||
// TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||
const {
|
||||
kdf,
|
||||
kdfIterations,
|
||||
kdfMemory,
|
||||
kdfParallelism,
|
||||
keyConnectorUrl: legacyKeyConnectorUrl,
|
||||
userDecryptionOptions,
|
||||
} = tokenResponse;
|
||||
const password = await this.keyGenerationService.createKey(512);
|
||||
const kdfConfig: KdfConfig =
|
||||
kdf === KdfType.PBKDF2_SHA256
|
||||
? new PBKDF2KdfConfig(kdfIterations)
|
||||
: new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
|
||||
|
||||
const masterKey = await this.keyService.makeMasterKey(
|
||||
password.keyB64,
|
||||
await this.tokenService.getEmail(),
|
||||
kdfConfig,
|
||||
);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
|
||||
const userKey = await this.keyService.makeUserKey(masterKey);
|
||||
await this.keyService.setUserKey(userKey[0], userId);
|
||||
await this.keyService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
|
||||
|
||||
const [pubKey, privKey] = await this.keyService.makeKeyPair(userKey[0]);
|
||||
|
||||
try {
|
||||
const keyConnectorUrl =
|
||||
legacyKeyConnectorUrl ?? userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl;
|
||||
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
|
||||
const keys = new KeysRequest(pubKey, privKey.encryptedString);
|
||||
const setPasswordRequest = new SetKeyConnectorKeyRequest(
|
||||
userKey[1].encryptedString,
|
||||
kdfConfig,
|
||||
orgId,
|
||||
keys,
|
||||
);
|
||||
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
|
||||
}
|
||||
|
||||
async setConvertAccountRequired(status: boolean, userId?: UserId) {
|
||||
await this.stateProvider.setUserState(CONVERT_ACCOUNT_TO_KEY_CONNECTOR, status, userId);
|
||||
}
|
||||
|
||||
getConvertAccountRequired(): Promise<boolean> {
|
||||
return firstValueFrom(this.convertAccountToKeyConnectorState.state$);
|
||||
}
|
||||
|
||||
async removeConvertAccountRequired(userId?: UserId) {
|
||||
await this.setConvertAccountRequired(null, userId);
|
||||
}
|
||||
|
||||
private handleKeyConnectorError(e: any) {
|
||||
this.logService.error(e);
|
||||
if (this.logoutCallback != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.logoutCallback("keyConnectorError");
|
||||
}
|
||||
throw new Error("Key Connector error");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user