Encourage The Use of UserId in CryptoService (#9033)

This commit is contained in:
Justin Baur 2024-05-04 02:04:56 -04:00 committed by GitHub
parent e4ef7d362e
commit 869fa29da6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 92 additions and 49 deletions

View File

@ -244,7 +244,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig);
await this.masterPasswordService.setMasterKey(masterKey, this.userId);
await this.cryptoService.setUserKey(userKey[0]);
await this.cryptoService.setUserKey(userKey[0], this.userId);
// Set private key only for new JIT provisioned users in MP encryption orgs
// Existing TDE users will have private key set on sync or on login
@ -253,7 +253,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
this.forceSetPasswordReason !=
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
) {
await this.cryptoService.setPrivateKey(keyPair[1].encryptedString);
await this.cryptoService.setPrivateKey(keyPair[1].encryptedString, this.userId);
}
const localMasterKeyHash = await this.cryptoService.hashMasterKey(

View File

@ -140,7 +140,7 @@ describe("AuthRequestLoginStrategy", () => {
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled();
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
});
it("sets keys after a successful authentication when only userKey provided in login credentials", async () => {
@ -164,7 +164,7 @@ describe("AuthRequestLoginStrategy", () => {
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
// trustDeviceIfRequired should be called
expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled();

View File

@ -161,9 +161,13 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}

View File

@ -252,7 +252,7 @@ export abstract class LoginStrategy {
await this.setMasterKey(response, userId);
await this.setUserKey(response, userId);
await this.setPrivateKey(response);
await this.setPrivateKey(response, userId);
this.messagingService.send("loggedIn");
@ -262,7 +262,7 @@ export abstract class LoginStrategy {
// The keys comes from different sources depending on the login strategy
protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setPrivateKey(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
@ -270,9 +270,10 @@ export abstract class LoginStrategy {
return false;
}
protected async createKeyPairForOldAccount() {
protected async createKeyPairForOldAccount(userId: UserId) {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport(userId);
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair(userKey);
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
return privateKey.encryptedString;
} catch (e) {

View File

@ -178,7 +178,7 @@ describe("PasswordLoginStrategy", () => {
userId,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
});
it("does not force the user to update their master password when there are no requirements", async () => {

View File

@ -233,9 +233,13 @@ export class PasswordLoginStrategy extends LoginStrategy {
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}

View File

@ -354,12 +354,16 @@ export class SsoLoginStrategy extends LoginStrategy {
await this.cryptoService.setUserKey(userKey);
}
protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
tokenResponse: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
const newSsoUser = tokenResponse.key == null;
if (!newSsoUser) {
await this.cryptoService.setPrivateKey(
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount()),
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}
}

View File

@ -159,7 +159,7 @@ describe("UserApiLoginStrategy", () => {
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
});
it("gets and sets the master key if Key Connector is enabled", async () => {

View File

@ -116,9 +116,13 @@ export class UserApiLoginStrategy extends LoginStrategy {
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}

View File

@ -224,7 +224,7 @@ describe("WebAuthnLoginStrategy", () => {
mockPrfPrivateKey,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey, userId);
// Master key and private key should not be set
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();

View File

@ -139,9 +139,13 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}

View File

@ -155,7 +155,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.cryptoService.setUserKey(userKey[0], userId);
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
const [pubKey, privKey] = await this.cryptoService.makeKeyPair(userKey[0]);
try {
const keyConnectorUrl =

View File

@ -1,11 +1,12 @@
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { UserId } from "../../types/guid";
import { BaseResponse } from "./base.response";
export class ProfileResponse extends BaseResponse {
id: string;
id: UserId;
name: string;
email: string;
emailVerified: boolean;

View File

@ -54,13 +54,23 @@ export abstract class CryptoService {
* for encryption of data instead of the user key.
*/
abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise<boolean>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
*
* @deprecated Please provide the userId of the user you want the user key for.
*/
abstract getUserKeyWithLegacySupport(): Promise<UserKey>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
* @param userId The desired user
*/
abstract getUserKeyWithLegacySupport(userId?: string): Promise<UserKey>;
abstract getUserKeyWithLegacySupport(userId: UserId): Promise<UserKey>;
/**
* Retrieves the user key from storage
* @param keySuffix The desired version of the user's key to retrieve
@ -169,10 +179,12 @@ export abstract class CryptoService {
* organization keys currently in memory
* @param orgs The organizations to set keys for
* @param providerOrgs The provider organizations to set keys for
* @param userId The user id of the user to set the org keys for
*/
abstract setOrgKeys(
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
userId: UserId,
): Promise<void>;
abstract activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
/**
@ -200,7 +212,13 @@ export abstract class CryptoService {
* @param providers The providers to set keys for
*/
abstract activeUserProviderKeys$: Observable<Record<ProviderId, ProviderKey>>;
abstract setProviderKeys(orgs: ProfileProviderResponse[]): Promise<void>;
/**
* Stores the provider keys for a given user.
* @param orgs The provider orgs for which to save the keys from.
* @param userId The user id of the user for which to store the keys for.
*/
abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>;
/**
* @param providerId The desired provider
* @returns The provider's symmetric key
@ -228,7 +246,7 @@ export abstract class CryptoService {
* Note: does not clear the private key if null is provided
* @param encPrivateKey An encrypted private key
*/
abstract setPrivateKey(encPrivateKey: string): Promise<void>;
abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Returns the private key from memory. If not available, decrypts it
* from storage and stores it in memory
@ -247,8 +265,9 @@ export abstract class CryptoService {
* @param key A key to encrypt the private key with. If not provided,
* defaults to the user key
* @returns A new keypair: [publicKey in Base64, encrypted privateKey]
* @throws If the provided key is a null-ish value.
*/
abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>;
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>;
/**
* @param pin The user's pin
* @param salt The user's salt

View File

@ -395,12 +395,11 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async setOrgKeys(
orgs: ProfileOrganizationResponse[] = [],
providerOrgs: ProfileProviderOrganizationResponse[] = [],
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
userId: UserId,
): Promise<void> {
// 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.activeUserEncryptedOrgKeysState.update((_) => {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).update(() => {
const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {};
orgs.forEach((org) => {
@ -450,8 +449,8 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId);
}
async setProviderKeys(providers: ProfileProviderResponse[]): Promise<void> {
await this.activeUserEncryptedProviderKeysState.update((_) => {
async setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).update(() => {
const encProviderKeys: { [providerId: ProviderId]: EncryptedString } = {};
providers.forEach((provider) => {
@ -494,12 +493,14 @@ export class CryptoService implements CryptoServiceAbstraction {
return [encShareKey, shareKey as T];
}
async setPrivateKey(encPrivateKey: EncryptedString): Promise<void> {
async setPrivateKey(encPrivateKey: EncryptedString, userId: UserId): Promise<void> {
if (encPrivateKey == null) {
return;
}
await this.activeUserEncryptedPrivateKeyState.update(() => encPrivateKey);
await this.stateProvider
.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => encPrivateKey);
}
async getPrivateKey(): Promise<Uint8Array> {
@ -523,9 +524,10 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.hashPhrase(userFingerprint);
}
async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> {
// Default to user key
key ||= await this.getUserKeyWithLegacySupport();
async makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]> {
if (key == null) {
throw new Error("'key' is a required parameter and must be non-null.");
}
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const publicB64 = Utils.fromBufferToB64(keyPair[0]);

View File

@ -35,7 +35,6 @@ import { SendData } from "../../../tools/send/models/data/send.data";
import { SendResponse } from "../../../tools/send/models/response/send.response";
import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction";
import { InternalSendService } from "../../../tools/send/services/send.service.abstraction";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction";
import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
@ -311,7 +310,7 @@ export class SyncService implements SyncServiceAbstraction {
}
private async syncProfile(response: ProfileResponse) {
const stamp = await this.tokenService.getSecurityStamp(response.id as UserId);
const stamp = await this.tokenService.getSecurityStamp(response.id);
if (stamp != null && stamp !== response.securityStamp) {
if (this.logoutCallback != null) {
await this.logoutCallback(true);
@ -321,15 +320,16 @@ export class SyncService implements SyncServiceAbstraction {
}
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
await this.cryptoService.setPrivateKey(response.privateKey);
await this.cryptoService.setProviderKeys(response.providers);
await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations);
await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor);
await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId);
await this.accountService.setAccountEmailVerified(
response.id as UserId,
response.emailVerified,
await this.cryptoService.setPrivateKey(response.privateKey, response.id);
await this.cryptoService.setProviderKeys(response.providers, response.id);
await this.cryptoService.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);
await this.billingAccountProfileStateService.setHasPremium(
response.premiumPersonally,