1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 19:23:52 +00:00

Merged branch with master and fixed conflicts

This commit is contained in:
gbubemismith
2023-08-23 22:45:39 -04:00
628 changed files with 16595 additions and 7728 deletions

View File

@@ -200,6 +200,7 @@ export abstract class ApiService {
postConvertToKeyConnector: () => Promise<void>;
//passwordless
postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
postAdminAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
@@ -523,7 +524,7 @@ export abstract class ApiService {
) => Promise<void>;
postResendSponsorshipOffer: (sponsoringOrgId: string) => Promise<void>;
getUserKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
getMasterKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
postUserKeyToKeyConnector: (
keyConnectorUrl: string,
request: KeyConnectorUserKeyRequest

View File

@@ -1,8 +0,0 @@
import { DeviceKey } from "../platform/models/domain/symmetric-crypto-key";
import { DeviceResponse } from "./devices/responses/device.response";
export abstract class DeviceCryptoServiceAbstraction {
trustDevice: () => Promise<DeviceResponse>;
getDeviceKey: () => Promise<DeviceKey>;
}

View File

@@ -1,14 +0,0 @@
import { DeviceResponse } from "./responses/device.response";
export abstract class DevicesApiServiceAbstraction {
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
updateTrustedDeviceKeys: (
deviceIdentifier: string,
devicePublicKeyEncryptedUserSymKey: string,
userSymKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
) => Promise<DeviceResponse>;
}

View File

@@ -0,0 +1,15 @@
import { Observable } from "rxjs";
import { DeviceView } from "./views/device.view";
export abstract class DevicesServiceAbstraction {
getDevices$: () => Observable<Array<DeviceView>>;
getDeviceByIdentifier$: (deviceIdentifier: string) => Observable<DeviceView>;
isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable<boolean>;
updateTrustedDeviceKeys$: (
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
) => Observable<DeviceView>;
}

View File

@@ -3,23 +3,20 @@ import { BaseResponse } from "../../../models/response/base.response";
export class DeviceResponse extends BaseResponse {
id: string;
name: number;
userId: string;
name: string;
identifier: string;
type: DeviceType;
creationDate: string;
encryptedUserKey: string;
encryptedPublicKey: string;
encryptedPrivateKey: string;
revisionDate: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.userId = this.getResponseProperty("UserId");
this.name = this.getResponseProperty("Name");
this.identifier = this.getResponseProperty("Identifier");
this.type = this.getResponseProperty("Type");
this.creationDate = this.getResponseProperty("CreationDate");
this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
this.encryptedPrivateKey = this.getResponseProperty("EncryptedPrivateKey");
this.revisionDate = this.getResponseProperty("RevisionDate");
}
}

View File

@@ -0,0 +1,17 @@
import { DeviceType } from "../../../enums";
import { View } from "../../../models/view/view";
import { DeviceResponse } from "../responses/device.response";
export class DeviceView implements View {
id: string;
userId: string;
name: string;
identifier: string;
type: DeviceType;
creationDate: string;
revisionDate: string;
constructor(deviceResponse: DeviceResponse) {
Object.assign(this, deviceResponse);
}
}

View File

@@ -0,0 +1,56 @@
import { Observable } from "rxjs";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { PinLockType } from "../../services/vault-timeout/vault-timeout-settings.service";
export abstract class VaultTimeoutSettingsService {
/**
* Set the vault timeout options for the user
* @param vaultTimeout The vault timeout in minutes
* @param vaultTimeoutAction The vault timeout action
* @param userId The user id to set. If not provided, the current user is used
*/
setVaultTimeoutOptions: (
vaultTimeout: number,
vaultTimeoutAction: VaultTimeoutAction
) => Promise<void>;
/**
* Get the available vault timeout actions for the current user
*
* **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes
* @param userId The user id to check. If not provided, the current user is used
*/
availableVaultTimeoutActions$: (userId?: string) => Observable<VaultTimeoutAction[]>;
/**
* Get the current vault timeout action for the user. This is not the same as the current state, it is
* calculated based on the current state, the user's policy, and the user's available unlock methods.
*/
getVaultTimeout: (userId?: string) => Promise<number>;
/**
* Observe the vault timeout action for the user. This is calculated based on users preferred lock action saved in the state,
* the user's policy, and the user's available unlock methods.
*
* **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes
* @param userId The user id to check. If not provided, the current user is used
*/
vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>;
/**
* Has the user enabled unlock with Pin.
* @param userId The user id to check. If not provided, the current user is used
* @returns PinLockType
*/
isPinLockSet: (userId?: string) => Promise<PinLockType>;
/**
* Has the user enabled unlock with Biometric.
* @param userId The user id to check. If not provided, the current user is used
* @returns boolean true if biometric lock is set
*/
isBiometricLockSet: (userId?: string) => Promise<boolean>;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -1,13 +0,0 @@
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
export abstract class VaultTimeoutSettingsService {
setVaultTimeoutOptions: (
vaultTimeout: number,
vaultTimeoutAction: VaultTimeoutAction
) => Promise<void>;
getVaultTimeout: (userId?: string) => Promise<number>;
getVaultTimeoutAction: (userId?: string) => Promise<VaultTimeoutAction>;
isPinLockSet: () => Promise<[boolean, boolean]>;
isBiometricLockSet: () => Promise<boolean>;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -0,0 +1,24 @@
import { UserKey, MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestResponse } from "../models/response/auth-request.response";
export abstract class AuthRequestCryptoServiceAbstraction {
setUserKeyAfterDecryptingSharedUserKey: (
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer
) => Promise<void>;
setKeysAfterDecryptingSharedMasterKeyAndHash: (
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer
) => Promise<void>;
decryptPubKeyEncryptedUserKey: (
pubKeyEncryptedUserKey: string,
privateKey: ArrayBuffer
) => Promise<UserKey>;
decryptPubKeyEncryptedMasterKeyAndHash: (
pubKeyEncryptedMasterKey: string,
pubKeyEncryptedMasterKeyHash: string,
privateKey: ArrayBuffer
) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>;
}

View File

@@ -1,7 +1,7 @@
import { Observable } from "rxjs";
import { AuthRequestPushNotification } from "../../models/response/notification.response";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { AuthenticationStatus } from "../enums/authentication-status";
import { AuthResult } from "../models/domain/auth-result";
import {
@@ -32,7 +32,7 @@ export abstract class AuthService {
captchaResponse: string
) => Promise<AuthResult>;
logOut: (callback: () => void) => void;
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
authingWithUserApiKey: () => boolean;
authingWithSso: () => boolean;
authingWithPassword: () => boolean;

View File

@@ -0,0 +1,25 @@
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { EncString } from "../../platform/models/domain/enc-string";
import { DeviceKey, UserKey } from "../../platform/models/domain/symmetric-crypto-key";
export abstract class DeviceTrustCryptoServiceAbstraction {
/**
* @description Retrieves the users choice to trust the device which can only happen after decryption
* Note: this value should only be used once and then reset
*/
getShouldTrustDevice: () => Promise<boolean | null>;
setShouldTrustDevice: (value: boolean) => Promise<void>;
trustDeviceIfRequired: () => Promise<void>;
trustDevice: () => Promise<DeviceResponse>;
getDeviceKey: () => Promise<DeviceKey>;
decryptUserKeyWithDeviceKey: (
encryptedDevicePrivateKey: EncString,
encryptedUserKey: EncString,
deviceKey?: DeviceKey
) => Promise<UserKey | null>;
rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>;
supportsDeviceTrust: () => Promise<boolean>;
}

View File

@@ -0,0 +1,27 @@
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { ListResponse } from "../../models/response/list.response";
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
export abstract class DevicesApiServiceAbstraction {
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
getDevices: () => Promise<ListResponse<DeviceResponse>>;
updateTrustedDeviceKeys: (
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
) => Promise<DeviceResponse>;
updateTrust: (updateDevicesTrustRequestModel: UpdateDevicesTrustRequest) => Promise<void>;
getDeviceKeys: (
deviceIdentifier: string,
secretVerificationRequest: SecretVerificationRequest
) => Promise<ProtectedDeviceResponse>;
}

View File

@@ -2,7 +2,7 @@ import { Organization } from "../../admin-console/models/domain/organization";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
export abstract class KeyConnectorService {
getAndSetKey: (url?: string) => Promise<void>;
setMasterKeyFromUrl: (url?: string) => Promise<void>;
getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>;

View File

@@ -0,0 +1,26 @@
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
export abstract class PasswordResetEnrollmentServiceAbstraction {
/*
* Checks the user's enrollment status and enrolls them if required
*/
abstract enrollIfRequired(organizationSsoIdentifier: string): Promise<void>;
/**
* Enroll current user in password reset
* @param organizationId - Organization in which to enroll the user
* @returns Promise that resolves when the user is enrolled
* @throws Error if the action fails
*/
abstract enroll(organizationId: string): Promise<void>;
/**
* Enroll user in password reset
* @param organizationId - Organization in which to enroll the user
* @param userId - User to enroll
* @param userKey - User's symmetric key
* @returns Promise that resolves when the user is enrolled
* @throws Error if the action fails
*/
abstract enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
}

View File

@@ -9,4 +9,16 @@ export abstract class UserVerificationService {
) => Promise<T>;
verifyUser: (verification: Verification) => Promise<boolean>;
requestOTP: () => Promise<void>;
/**
* Check if user has master password or only uses passwordless technologies to log in
* @param userId The user id to check. If not provided, the current user is used
* @returns True if the user has a master password
*/
hasMasterPassword: (userId?: string) => Promise<boolean>;
/**
* Check if the user has a master password and has used it during their current session
* @param userId The user id to check. If not provided, the current user id used
* @returns True if the user has a master password and has used it in the current session
*/
hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise<boolean>;
}

View File

@@ -1,4 +1,5 @@
export enum AuthRequestType {
AuthenticateAndUnlock = 0,
Unlock = 1,
AdminApproval = 2,
}

View File

@@ -9,12 +9,25 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { Account, AccountProfile, AccountTokens } from "../../platform/models/domain/account";
import {
Account,
AccountDecryptionOptions,
AccountKeys,
AccountProfile,
AccountTokens,
} from "../../platform/models/domain/account";
import { EncString } from "../../platform/models/domain/enc-string";
import {
DeviceKey,
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import {
PasswordStrengthService,
PasswordStrengthServiceAbstraction,
} from "../../tools/password-strength";
import { CsprngArray } from "../../types/csprng";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -28,6 +41,10 @@ import { IdentityCaptchaResponse } from "../models/response/identity-captcha.res
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
import {
IUserDecryptionOptionsServerResponse,
UserDecryptionOptionsResponse,
} from "../models/response/user-decryption-options/user-decryption-options.response";
import { PasswordLogInStrategy } from "./password-login.strategy";
@@ -37,7 +54,7 @@ const masterPassword = "password";
const deviceId = Utils.newGuid();
const accessToken = "ACCESS_TOKEN";
const refreshToken = "REFRESH_TOKEN";
const encKey = "ENC_KEY";
const userKey = "USER_KEY";
const privateKey = "PRIVATE_KEY";
const captchaSiteKey = "CAPTCHA_SITE_KEY";
const kdf = 0;
@@ -45,6 +62,13 @@ const kdfIterations = 10000;
const userId = Utils.newGuid();
const masterPasswordHash = "MASTER_PASSWORD_HASH";
const name = "NAME";
const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = {
HasMasterPassword: true,
};
const userDecryptionOptions = new UserDecryptionOptionsResponse(
defaultUserDecryptionOptionsServerResponse
);
const acctDecryptionOptions = AccountDecryptionOptions.fromResponse(userDecryptionOptions);
const decodedToken = {
sub: userId,
@@ -58,13 +82,14 @@ const twoFactorToken = "TWO_FACTOR_TOKEN";
const twoFactorRemember = true;
export function identityTokenResponseFactory(
masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null
masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null,
userDecryptionOptions: IUserDecryptionOptionsServerResponse = null
) {
return new IdentityTokenResponse({
ForcePasswordReset: false,
Kdf: kdf,
KdfIterations: kdfIterations,
Key: encKey,
Key: userKey,
PrivateKey: privateKey,
ResetMasterPassword: false,
access_token: accessToken,
@@ -73,9 +98,11 @@ export function identityTokenResponseFactory(
scope: "api offline_access",
token_type: "Bearer",
MasterPasswordPolicy: masterPasswordPolicyResponse,
UserDecryptionOptions: userDecryptionOptions || defaultUserDecryptionOptionsServerResponse,
});
}
// TODO: add tests for latest changes to base class for TDE
describe("LogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
@@ -129,8 +156,23 @@ describe("LogInStrategy", () => {
});
describe("base class", () => {
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
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;
});
it("sets the local environment after a successful login with master password", async () => {
const idTokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
await passwordLogInStrategy.logIn(credentials);
@@ -154,13 +196,36 @@ describe("LogInStrategy", () => {
refreshToken: refreshToken,
},
},
keys: new AccountKeys(),
decryptionOptions: acctDecryptionOptions,
})
);
expect(cryptoService.setEncKey).toHaveBeenCalledWith(encKey);
expect(cryptoService.setEncPrivateKey).toHaveBeenCalledWith(privateKey);
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
});
it("persists a device key for trusted device encryption when it exists on login", async () => {
// Arrange
const idTokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
const deviceKey = new SymmetricCryptoKey(
new Uint8Array(userKeyBytesLength).buffer as CsprngArray
) as DeviceKey;
stateService.getDeviceKey.mockResolvedValue(deviceKey);
const accountKeys = new AccountKeys();
accountKeys.deviceKey = deviceKey;
// Act
await passwordLogInStrategy.logIn(credentials);
// Assert
expect(stateService.addAccount).toHaveBeenCalledWith(
expect.objectContaining({ keys: accountKeys })
);
});
it("builds AuthResult", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.forcePasswordReset = true;
@@ -187,6 +252,8 @@ describe("LogInStrategy", () => {
});
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
const result = await passwordLogInStrategy.logIn(credentials);
@@ -204,13 +271,15 @@ describe("LogInStrategy", () => {
cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await passwordLogInStrategy.logIn(credentials);
// User key must be set before the new RSA keypair is generated, otherwise we can't decrypt the EncKey
expect(cryptoService.setKey).toHaveBeenCalled();
// User symmetric key must be set before the new RSA keypair is generated
expect(cryptoService.setUserKey).toHaveBeenCalled();
expect(cryptoService.makeKeyPair).toHaveBeenCalled();
expect(cryptoService.setKey.mock.invocationCallOrder[0]).toBeLessThan(
expect(cryptoService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan(
cryptoService.makeKeyPair.mock.invocationCallOrder[0]
);

View File

@@ -6,7 +6,13 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Account, AccountProfile, AccountTokens } from "../../platform/models/domain/account";
import {
Account,
AccountDecryptionOptions,
AccountKeys,
AccountProfile,
AccountTokens,
} from "../../platform/models/domain/account";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
@@ -53,9 +59,6 @@ export abstract class LogInStrategy {
| PasswordlessLogInCredentials
): Promise<AuthResult>;
// The user key comes from different sources depending on the login strategy
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string = null
@@ -101,12 +104,28 @@ export abstract class LogInStrategy {
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
// Must persist existing device key if it exists for trusted device decryption to work
// However, we must provide a user id so that the device key can be retrieved
// as the state service won't have an active account at this point in time
// even though the data exists in local storage.
const userId = accountInformation.sub;
const deviceKey = await this.stateService.getDeviceKey({ userId });
const accountKeys = new AccountKeys();
if (deviceKey) {
accountKeys.deviceKey = deviceKey;
}
// If you don't persist existing admin auth requests on login, they will get deleted.
const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId });
await this.stateService.addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: accountInformation.sub,
userId,
name: accountInformation.name,
email: accountInformation.email,
hasPremiumPersonally: accountInformation.premium,
@@ -123,6 +142,11 @@ export abstract class LogInStrategy {
refreshToken: tokenResponse.refreshToken,
},
},
keys: accountKeys,
decryptionOptions: AccountDecryptionOptions.fromResponse(
tokenResponse.userDecryptionOptions
),
adminAuthRequest: adminAuthRequest?.toJSON(),
})
);
}
@@ -135,28 +159,41 @@ export abstract class LogInStrategy {
result.forcePasswordReset = ForceResetPasswordReason.AdminForcePasswordReset;
}
// Must come before setting keys, user key needs email to update additional keys
await this.saveAccountInformation(response);
if (response.twoFactorToken != null) {
await this.tokenService.setTwoFactorToken(response);
}
await this.setMasterKey(response);
await this.setUserKey(response);
// Must come after the user Key is set, otherwise createKeyPairForOldAccount will fail
const newSsoUser = response.key == null;
if (!newSsoUser) {
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
await this.setPrivateKey(response);
this.messagingService.send("loggedIn");
return result;
}
// The keys comes from different sources depending on the login strategy
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
protected async createKeyPairForOldAccount() {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
return privateKey.encryptedString;
} catch (e) {
this.logService.error(e);
}
}
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
const result = new AuthResult();
result.twoFactorProviders = response.twoFactorProviders2;
@@ -173,14 +210,4 @@ export abstract class LogInStrategy {
result.captchaSiteKey = response.siteKey;
return result;
}
private async createKeyPairForOldAccount() {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
return privateKey.encryptedString;
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -10,17 +10,23 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import {
PasswordStrengthService,
PasswordStrengthServiceAbstraction,
} from "../../tools/password-strength";
import { CsprngArray } from "../../types/csprng";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
@@ -31,11 +37,11 @@ const email = "hello@world.com";
const masterPassword = "password";
const hashedPassword = "HASHED_PASSWORD";
const localHashedPassword = "LOCAL_HASHED_PASSWORD";
const preloginKey = new SymmetricCryptoKey(
const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
)
);
) as MasterKey;
const deviceId = Utils.newGuid();
const masterPasswordPolicy = new MasterPasswordPolicyResponse({
EnforceOnLogin: true,
@@ -58,6 +64,7 @@ describe("PasswordLogInStrategy", () => {
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
let tokenResponse: IdentityTokenResponse;
beforeEach(async () => {
cryptoService = mock<CryptoService>();
@@ -76,12 +83,12 @@ describe("PasswordLogInStrategy", () => {
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
authService.makePreloginKey.mockResolvedValue(preloginKey);
authService.makePreloginKey.mockResolvedValue(masterKey);
cryptoService.hashPassword
cryptoService.hashMasterKey
.calledWith(masterPassword, expect.anything(), undefined)
.mockResolvedValue(hashedPassword);
cryptoService.hashPassword
cryptoService.hashMasterKey
.calledWith(masterPassword, expect.anything(), HashPurpose.LocalAuthorization)
.mockResolvedValue(localHashedPassword);
@@ -102,10 +109,9 @@ describe("PasswordLogInStrategy", () => {
authService
);
credentials = new PasswordLogInCredentials(email, masterPassword);
tokenResponse = identityTokenResponseFactory(masterPasswordPolicy);
apiService.postIdentityToken.mockResolvedValue(
identityTokenResponseFactory(masterPasswordPolicy)
);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
});
it("sends master password credentials to the server", async () => {
@@ -127,15 +133,23 @@ describe("PasswordLogInStrategy", () => {
);
});
it("sets the local environment after a successful login", async () => {
it("sets keys after a successful authentication", async () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await passwordLogInStrategy.logIn(credentials);
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey);
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword);
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(localHashedPassword);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
});
it("does not force the user to update their master password when there are no requirements", async () => {
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory(null));
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory());
const result = await passwordLogInStrategy.logIn(credentials);

View File

@@ -8,7 +8,7 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
@@ -35,8 +35,8 @@ export class PasswordLogInStrategy extends LogInStrategy {
tokenRequest: PasswordTokenRequest;
private localHashedPassword: string;
private key: SymmetricCryptoKey;
private localMasterKeyHash: string;
private masterKey: MasterKey;
/**
* Options to track if the user needs to update their password due to a password that does not meet an organization's
@@ -71,12 +71,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
);
}
async setUserKey() {
await this.cryptoService.setKey(this.key);
await this.cryptoService.setKeyHash(this.localHashedPassword);
}
async logInTwoFactor(
override async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string
): Promise<AuthResult> {
@@ -96,28 +91,29 @@ export class PasswordLogInStrategy extends LogInStrategy {
return result;
}
async logIn(credentials: PasswordLogInCredentials) {
override async logIn(credentials: PasswordLogInCredentials) {
const { email, masterPassword, captchaToken, twoFactor } = credentials;
this.key = await this.authService.makePreloginKey(masterPassword, email);
this.masterKey = await this.authService.makePreloginKey(masterPassword, email);
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
this.localHashedPassword = await this.cryptoService.hashPassword(
this.localMasterKeyHash = await this.cryptoService.hashMasterKey(
masterPassword,
this.key,
this.masterKey,
HashPurpose.LocalAuthorization
);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key);
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, this.masterKey);
this.tokenRequest = new PasswordTokenRequest(
email,
hashedPassword,
masterKeyHash,
captchaToken,
await this.buildTwoFactor(twoFactor),
await this.buildDeviceRequest()
);
const [authResult, identityResponse] = await this.startLogIn();
const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
@@ -145,6 +141,27 @@ export class PasswordLogInStrategy extends LogInStrategy {
return authResult;
}
protected override async setMasterKey(response: IdentityTokenResponse) {
await this.cryptoService.setMasterKey(this.masterKey);
await this.cryptoService.setMasterKeyHash(this.localMasterKeyHash);
}
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
const masterKey = await this.cryptoService.getMasterKey();
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
): MasterPasswordPolicyOptions {

View File

@@ -0,0 +1,138 @@
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { identityTokenResponseFactory } from "./login.strategy.spec";
import { PasswordlessLogInStrategy } from "./passwordless-login.strategy";
describe("PasswordlessLogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
let appIdService: MockProxy<AppIdService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let passwordlessLoginStrategy: PasswordlessLogInStrategy;
let credentials: PasswordlessLogInCredentials;
let tokenResponse: IdentityTokenResponse;
const deviceId = Utils.newGuid();
const email = "EMAIL";
const accessCode = "ACCESS_CODE";
const authRequestId = "AUTH_REQUEST_ID";
const decMasterKey = new SymmetricCryptoKey(
new Uint8Array(64).buffer as CsprngArray
) as MasterKey;
const decUserKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
const decMasterKeyHash = "LOCAL_PASSWORD_HASH";
beforeEach(async () => {
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
messagingService = mock<MessagingService>();
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
passwordlessLoginStrategy = new PasswordlessLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
deviceTrustCryptoService
);
tokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
});
it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => {
credentials = new PasswordlessLogInCredentials(
email,
accessCode,
authRequestId,
null,
decMasterKey,
decMasterKeyHash
);
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await passwordlessLoginStrategy.logIn(credentials);
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled();
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
});
it("sets keys after a successful authentication when only userKey provided in login credentials", async () => {
// Initialize credentials with only userKey
credentials = new PasswordlessLogInCredentials(
email,
accessCode,
authRequestId,
decUserKey, // Pass userKey
null, // No masterKey
null // No masterKeyHash
);
// Call logIn
await passwordlessLoginStrategy.logIn(credentials);
// setMasterKey and setMasterKeyHash should not be called
expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
expect(cryptoService.setMasterKeyHash).not.toHaveBeenCalled();
// 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);
// trustDeviceIfRequired should be called
expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled();
});
});

View File

@@ -5,13 +5,14 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { AuthService } from "../abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { AuthResult } from "../models/domain/auth-result";
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { LogInStrategy } from "./login.strategy";
@@ -41,7 +42,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private authService: AuthService
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
) {
super(
cryptoService,
@@ -56,20 +57,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
);
}
async setUserKey() {
await this.cryptoService.setKey(this.passwordlessCredentials.decKey);
await this.cryptoService.setKeyHash(this.passwordlessCredentials.localPasswordHash);
}
async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
}
async logIn(credentials: PasswordlessLogInCredentials) {
override async logIn(credentials: PasswordlessLogInCredentials) {
this.passwordlessCredentials = credentials;
this.tokenRequest = new PasswordTokenRequest(
@@ -84,4 +72,52 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
const [authResult] = await this.startLogIn();
return authResult;
}
override async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
}
protected override async setMasterKey(response: IdentityTokenResponse) {
if (
this.passwordlessCredentials.decryptedMasterKey &&
this.passwordlessCredentials.decryptedMasterKeyHash
) {
await this.cryptoService.setMasterKey(this.passwordlessCredentials.decryptedMasterKey);
await this.cryptoService.setMasterKeyHash(
this.passwordlessCredentials.decryptedMasterKeyHash
);
}
}
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
// User now may or may not have a master password
// but set the master key encrypted user key if it exists regardless
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
if (this.passwordlessCredentials.decryptedUserKey) {
await this.cryptoService.setUserKey(this.passwordlessCredentials.decryptedUserKey);
} else {
await this.trySetUserKeyWithMasterKey();
// Establish trust if required after setting user key
await this.deviceTrustCryptoService.trustDeviceIfRequired();
}
}
private async trySetUserKeyWithMasterKey(): Promise<void> {
const masterKey = await this.cryptoService.getMasterKey();
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
}

View File

@@ -3,19 +3,34 @@ import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import {
DeviceKey,
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { SsoLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IUserDecryptionOptionsServerResponse } from "../models/response/user-decryption-options/user-decryption-options.response";
import { identityTokenResponseFactory } from "./login.strategy.spec";
import { SsoLogInStrategy } from "./sso-login.strategy";
// TODO: Add tests for new trySetUserKeyWithApprovedAdminRequestIfExists logic
// https://bitwarden.atlassian.net/browse/PM-3339
describe("SsoLogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
@@ -27,6 +42,9 @@ describe("SsoLogInStrategy", () => {
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestCryptoService: MockProxy<AuthRequestCryptoServiceAbstraction>;
let i18nService: MockProxy<I18nService>;
let ssoLogInStrategy: SsoLogInStrategy;
let credentials: SsoLogInCredentials;
@@ -50,6 +68,9 @@ describe("SsoLogInStrategy", () => {
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
keyConnectorService = mock<KeyConnectorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestCryptoService = mock<AuthRequestCryptoServiceAbstraction>();
i18nService = mock<I18nService>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
@@ -65,7 +86,10 @@ describe("SsoLogInStrategy", () => {
logService,
stateService,
twoFactorService,
keyConnectorService
keyConnectorService,
deviceTrustCryptoService,
authRequestCryptoService,
i18nService
);
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
});
@@ -98,33 +122,194 @@ describe("SsoLogInStrategy", () => {
await ssoLogInStrategy.logIn(credentials);
expect(cryptoService.setEncPrivateKey).not.toHaveBeenCalled();
expect(cryptoService.setEncKey).not.toHaveBeenCalled();
expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
expect(cryptoService.setPrivateKey).not.toHaveBeenCalled();
});
it("gets and sets KeyConnector key for enrolled user", async () => {
it("sets master key encrypted user key for existing SSO users", async () => {
// Arrange
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
// Act
await ssoLogInStrategy.logIn(credentials);
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
// Assert
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
});
it("converts new SSO user to Key Connector on first login", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
tokenResponse.key = null;
describe("Trusted Device Decryption", () => {
const deviceKeyBytesLength = 64;
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
const mockDeviceKey: DeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
const userKeyBytesLength = 64;
const mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength).buffer as CsprngArray;
const mockUserKey: UserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
await ssoLogInStrategy.logIn(credentials);
const mockEncDevicePrivateKey =
"2.eh465OrUcluL9UpnCOUTAg==|2HXNXwrLwAjUfZ/U75c92rZEltt1eHxjMkp/ADAmx346oT1+GaQvaL1QIV/9Om0T72m8AnlO92iUfWdhbA/ifHZ+lhFoUVeyw1M88CMzktbVcq42rFoK7SGHSAGdTL3ccUWKI8yCCQJhpt2X6a/5+T7ey5k2CqvylKyOtkiCnVeLmYqETn5BM9Rl3tEgJW1yDLuSJ+L+Qh9xnk/Z3zJUV5HAs+YwjKwuSNrd00SXjDyx8rBEstD9MKI+lrk7to/q90vqKqCucAj/dzUpVtHe88al2AAlBVwQ13HUPdNFOyti6niUgCAWx+DzRqlhkFvl/z/rtxtQsyqq/3Eh/EL54ylxKzAya0ev9EaIOm/dD1aBmI58p4Bs0eMOCIKJjtw+Cmdql+RhCtKtumgFShqyXv+LfD/FgUsdTVNExk3YNhgwPR4jOaMa/j9LCrBMCLKxdAhQyBe7T3qoX1fBBirvY6t77ifMu1YEQ6DfmFphVSwDH5C9xGeTSh5IELSf0tGVtlWUe9RffDDzccD0L1lR8U+dqzoSTYCuXvhEhQptdIW6fpH/47u0M5MiI97/d35A7Et2I1gjHp7WF3qsY20ellBueu7ZL5P1BmqPXl58yaBBXJaCutYHDfIucspqdZmfBGEbdRT4wmuZRON0J8zLmUejM0VR/2MOmpfyYQXnJhTfrvnZ1bOg1aMhUxJ2vhDNPXUFm5b+vwsho4GEvcLAKq9WwbvOJ/sK7sEVfTfEO2IG+0X6wkWm7RpR6Wq9FGKSrv2PSjMAYnb+z3ETeWiaaiD+tVFxa2AaqsbOuX092/86GySpHES7cFWhQ/YMOgj6egUi8mEC0CqMXYsx0TTJDsn16oP+XB3a2WoRqzE0YBozp2aMXxhVf/jMZ03BmEmRQu5B+Sq1gMEZwtIfJ+srkZLMYlLjvVw92FRoFy+N6ytPiyf6RMHMUnJ3vEZSBogaElYoQAtFJ5kK811CUzb78zEHH8xWtPrCZn9zZfvf/zaWxo7fpV8VwAwUeHXHcQMraZum5QeO+5tLRUYrLm85JNelGfmUA3BjfNyFbfb32PhkWWd0CbDaPME48uIriVK32pNEtvtR/+I/f3YgA/jP9kSlDvbzG/OAg/AFBIpNwKUzsu4+va8mI+O5FDufw5D74WwdGJ9DeyEb2CHtWMR1VwtFKL0ZZsqltNf8EkBeJ5RtTNtAMM8ie4dDZaKC96ymQHKrdB4hjkAr0F1XFsU4XdOa9Nbkdcm/7KoNc6bE6oJtG9lqE8h+1CysfcbfJ7am+hvDFzT0IPmp3GDSMAk+e6xySgFQw0C/SZ7LQsxPa1s6hc+BOtTn0oClZnU7Mowxv+z+xURJj4Yp3Cy6tAoia1jEQSs6lSMNKPf9bi3xFKtPl4143hwhpvTAzJUcski9OVGd7Du+VyxwIrvLqp5Ct/oNrESVJpf1EDCs9xT1EW+PiSkRmHXoZ1t5MOLFEiMAZL2+bNe3A2661oJeMtps8zrfCVc251OUE1WvqWePlTOs5TDVqdwDH88J6rHLsbaf33Mxh5DP8gMfZQxE44Nsp6H0/Szfkss5UmFwBEpHjl1GJMWDnB3u2d+l1CSkLoB6C+diAUlY6wL/VwJBeMPHZTf6amQIS2B/lo/CnvV/E3k=|uuoY4b7xwMYBNIZi85KBsaHmNqtJl5FrKxZI9ugeNwc=";
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
tokenResponse,
ssoOrgId
);
const mockEncUserKey =
"4.Xht6K9GA9jKcSNy4TaIvdj7f9+WsgQycs/HdkrJi33aC//roKkjf3UTGpdzFLxVP3WhyOVGyo9f2Jymf1MFPdpg7AuMnpGJlcrWLDbnPjOJo4x5gUwwBUmy3nFw6+wamyS1LRmrBPcv56yKpf80k5Q3hUrum8q9YS9m2I10vklX/TaB1YML0yo+K1feWUxg8vIx+vloxhUdkkysvcV5xU3R+AgYLrwvJS8TLL7Ug/P5HxinCaIroRrNe8xcv84vyVnzPFdXe0cfZ0cpcrm586LwfEXP2seeldO/bC51Uk/mudeSALJURPC64f5ch2cOvk48GOTapGnssCqr6ky5yFw==";
const userDecryptionOptsServerResponseWithTdeOption: IUserDecryptionOptionsServerResponse = {
HasMasterPassword: true,
TrustedDeviceOption: {
HasAdminApproval: true,
HasLoginApprovingDevice: true,
HasManageResetPasswordPermission: false,
EncryptedPrivateKey: mockEncDevicePrivateKey,
EncryptedUserKey: mockEncUserKey,
},
};
const mockIdTokenResponseWithModifiedTrustedDeviceOption = (key: string, value: any) => {
const userDecryptionOpts: IUserDecryptionOptionsServerResponse = {
...userDecryptionOptsServerResponseWithTdeOption,
TrustedDeviceOption: {
...userDecryptionOptsServerResponseWithTdeOption.TrustedDeviceOption,
[key]: value,
},
};
return identityTokenResponseFactory(null, userDecryptionOpts);
};
beforeEach(() => {
jest.clearAllMocks();
});
it("decrypts and sets user key when trusted device decryption option exists with valid device key and enc key data", async () => {
// Arrange
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
null,
userDecryptionOptsServerResponseWithTdeOption
);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey);
const cryptoSvcSetUserKeySpy = jest.spyOn(cryptoService, "setUserKey");
// Act
await ssoLogInStrategy.logIn(credentials);
// Assert
expect(deviceTrustCryptoService.getDeviceKey).toHaveBeenCalledTimes(1);
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1);
expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledWith(mockUserKey);
});
it("does not set the user key when deviceKey is missing", async () => {
// Arrange
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
null,
userDecryptionOptsServerResponseWithTdeOption
);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
// Set deviceKey to be null
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(null);
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey);
// Act
await ssoLogInStrategy.logIn(credentials);
// Assert
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
});
describe.each([
{
valueName: "encDevicePrivateKey",
},
{
valueName: "encUserKey",
},
])("given trusted device decryption option has missing encrypted key data", ({ valueName }) => {
it(`does not set the user key when ${valueName} is missing`, async () => {
// Arrange
const idTokenResponse = mockIdTokenResponseWithModifiedTrustedDeviceOption(valueName, null);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
// Act
await ssoLogInStrategy.logIn(credentials);
// Assert
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
});
});
it("does not set user key when decrypted user key is null", async () => {
// Arrange
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
null,
userDecryptionOptsServerResponseWithTdeOption
);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
// Set userKey to be null
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(null);
// Act
await ssoLogInStrategy.logIn(credentials);
// Assert
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
});
});
describe("Key Connector", () => {
let tokenResponse: IdentityTokenResponse;
beforeEach(() => {
tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
});
it("gets and sets the master key if Key Connector is enabled", async () => {
const masterKey = new SymmetricCryptoKey(
new Uint8Array(64).buffer as CsprngArray
) as MasterKey;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
await ssoLogInStrategy.logIn(credentials);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
});
it("converts new SSO user to Key Connector on first login", async () => {
tokenResponse.key = null;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
tokenResponse,
ssoOrgId
);
});
it("decrypts and sets the user key if Key Connector is enabled", async () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
const masterKey = new SymmetricCryptoKey(
new Uint8Array(64).buffer as CsprngArray
) as MasterKey;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await ssoLogInStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
});
});
});

View File

@@ -1,10 +1,16 @@
import { ApiService } from "../../abstractions/api.service";
import { AuthRequestResponse } from "../../auth/models/response/auth-request.response";
import { HttpStatusCode } from "../../enums";
import { ErrorResponse } from "../../models/response/error.response";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -34,7 +40,10 @@ export class SsoLogInStrategy extends LogInStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private keyConnectorService: KeyConnectorService
private keyConnectorService: KeyConnectorService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authReqCryptoService: AuthRequestCryptoServiceAbstraction,
private i18nService: I18nService
) {
super(
cryptoService,
@@ -49,18 +58,6 @@ export class SsoLogInStrategy extends LogInStrategy {
);
}
async setUserKey(tokenResponse: IdentityTokenResponse) {
const newSsoUser = tokenResponse.key == null;
if (tokenResponse.keyConnectorUrl != null) {
if (!newSsoUser) {
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
} else {
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
}
}
}
async logIn(credentials: SsoLogInCredentials) {
this.orgId = credentials.orgId;
this.tokenRequest = new SsoTokenRequest(
@@ -78,4 +75,155 @@ export class SsoLogInStrategy extends LogInStrategy {
return ssoAuthResult;
}
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
// TODO: discuss how this is no longer true with TDE
// eventually well need to support migration of existing TDE users to Key Connector
const newSsoUser = tokenResponse.key == null;
if (tokenResponse.keyConnectorUrl != null) {
if (!newSsoUser) {
await this.keyConnectorService.setMasterKeyFromUrl(tokenResponse.keyConnectorUrl);
} else {
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
}
}
}
// TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request)
// so might be worth moving this logic to a common place (base login strategy or a separate service?)
protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise<void> {
const masterKeyEncryptedUserKey = tokenResponse.key;
// Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users
// on account creation and subsequent logins (confirmed or unconfirmed)
// but that is fine for TDE so we cannot return if it is undefined
if (masterKeyEncryptedUserKey) {
// set the master key encrypted user key if it exists
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
}
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
// Note: TDE and key connector are mutually exclusive
if (userDecryptionOptions?.trustedDeviceOption) {
await this.trySetUserKeyWithApprovedAdminRequestIfExists();
const hasUserKey = await this.cryptoService.hasUserKey();
// Only try to set user key with device key if admin approval request was not successful
if (!hasUserKey) {
await this.trySetUserKeyWithDeviceKey(tokenResponse);
}
} else if (
// TODO: remove tokenResponse.keyConnectorUrl when it's deprecated
masterKeyEncryptedUserKey != null &&
(tokenResponse.keyConnectorUrl || userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl)
) {
// Key connector enabled for user
await this.trySetUserKeyWithMasterKey();
}
// Note: In the traditional SSO flow with MP without key connector, the lock component
// is responsible for deriving master key from MP entry and then decrypting the user key
}
private async trySetUserKeyWithApprovedAdminRequestIfExists(): Promise<void> {
// At this point a user could have an admin auth request that has been approved
const adminAuthReqStorable = await this.stateService.getAdminAuthRequest();
if (!adminAuthReqStorable) {
return;
}
// Call server to see if admin auth request has been approved
let adminAuthReqResponse: AuthRequestResponse;
try {
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
// if we get a 404, it means the auth request has been deleted so clear it from storage
await this.stateService.setAdminAuthRequest(null);
}
// Always return on an error here as we don't want to block the user from logging in
return;
}
if (adminAuthReqResponse?.requestApproved) {
// if masterPasswordHash has a value, we will always receive authReqResponse.key
// as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
if (adminAuthReqResponse.masterPasswordHash) {
await this.authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
adminAuthReqResponse,
adminAuthReqStorable.privateKey
);
} else {
// if masterPasswordHash is null, we will always receive authReqResponse.key
// as authRequestPublicKey(userKey)
await this.authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
adminAuthReqResponse,
adminAuthReqStorable.privateKey
);
}
if (await this.cryptoService.hasUserKey()) {
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
await this.deviceTrustCryptoService.trustDeviceIfRequired();
// if we successfully decrypted the user key, we can delete the admin auth request out of state
// TODO: eventually we post and clean up DB as well once consumed on client
await this.stateService.setAdminAuthRequest(null);
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
}
}
}
private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> {
const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption;
const deviceKey = await this.deviceTrustCryptoService.getDeviceKey();
const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey;
const encUserKey = trustedDeviceOption?.encryptedUserKey;
if (!deviceKey || !encDevicePrivateKey || !encUserKey) {
return;
}
const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
encDevicePrivateKey,
encUserKey,
deviceKey
);
if (userKey) {
await this.cryptoService.setUserKey(userKey);
}
}
private async trySetUserKeyWithMasterKey(): Promise<void> {
const masterKey = await this.cryptoService.getMasterKey();
if (!masterKey) {
throw new Error("Master key not found");
}
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise<void> {
const newSsoUser = tokenResponse.key == null;
if (!newSsoUser) {
await this.cryptoService.setPrivateKey(
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
}
}

View File

@@ -9,6 +9,12 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -101,7 +107,18 @@ describe("UserApiLogInStrategy", () => {
expect(stateService.addAccount).toHaveBeenCalled();
});
it("gets and sets the Key Connector key from environmentUrl", async () => {
it("sets the encrypted user key and private key from the identity token response", async () => {
const tokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
});
it("gets and sets the master key if Key Connector is enabled", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
@@ -110,6 +127,24 @@ describe("UserApiLogInStrategy", () => {
await apiLogInStrategy.logIn(credentials);
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
});
it("decrypts and sets the user key if Key Connector is enabled", async () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
});
});

View File

@@ -44,14 +44,7 @@ export class UserApiLogInStrategy extends LogInStrategy {
);
}
async setUserKey(tokenResponse: IdentityTokenResponse) {
if (tokenResponse.apiUseKeyConnector) {
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
}
}
async logIn(credentials: UserApiLogInCredentials) {
override async logIn(credentials: UserApiLogInCredentials) {
this.tokenRequest = new UserApiTokenRequest(
credentials.clientId,
credentials.clientSecret,
@@ -63,6 +56,31 @@ export class UserApiLogInStrategy extends LogInStrategy {
return authResult;
}
protected override async setMasterKey(response: IdentityTokenResponse) {
if (response.apiUseKeyConnector) {
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
}
}
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
if (response.apiUseKeyConnector) {
const masterKey = await this.cryptoService.getMasterKey();
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
await super.saveAccountInformation(tokenResponse);
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);

View File

@@ -0,0 +1,41 @@
import { Utils } from "../../../platform/misc/utils";
// TODO: Tech Debt: potentially create a type Storage shape vs using a class here in the future
// type StorageShape {
// id: string;
// privateKey: string;
// }
// so we can get rid of the any type passed into fromJSON and coming out of ToJSON
export class AdminAuthRequestStorable {
id: string;
privateKey: Uint8Array;
constructor(init?: Partial<AdminAuthRequestStorable>) {
if (init) {
Object.assign(this, init);
}
}
toJSON() {
return {
id: this.id,
privateKey: Utils.fromBufferToByteString(this.privateKey),
};
}
static fromJSON(obj: any): AdminAuthRequestStorable {
if (obj == null) {
return null;
}
let privateKeyBuffer = null;
if (obj.privateKey) {
privateKeyBuffer = Utils.fromByteStringToArray(obj.privateKey);
}
return new AdminAuthRequestStorable({
id: obj.id,
privateKey: privateKeyBuffer,
});
}
}

View File

@@ -5,7 +5,14 @@ import { ForceResetPasswordReason } from "./force-reset-password-reason";
export class AuthResult {
captchaSiteKey = "";
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
/**
* @deprecated
* Replace with using AccountDecryptionOptions to determine if the user does
* not have a master password and is not using Key Connector.
* */
resetMasterPassword = false;
forcePasswordReset: ForceResetPasswordReason = ForceResetPasswordReason.None;
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
ssoEmail2FaSessionToken?: string;

View File

@@ -1,4 +1,4 @@
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { MasterKey, UserKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { AuthenticationType } from "../../enums/authentication-type";
import { TokenTwoFactorRequest } from "../request/identity-token/token-two-factor.request";
@@ -38,8 +38,9 @@ export class PasswordlessLogInCredentials {
public email: string,
public accessCode: string,
public authRequestId: string,
public decKey: SymmetricCryptoKey,
public localPasswordHash: string,
public decryptedUserKey: UserKey,
public decryptedMasterKey: MasterKey,
public decryptedMasterKeyHash: string,
public twoFactor?: TokenTwoFactorRequest
) {}
}

View File

@@ -0,0 +1,3 @@
export class KeyConnectorUserDecryptionOption {
constructor(public keyConnectorUrl: string) {}
}

View File

@@ -0,0 +1,7 @@
export class TrustedDeviceUserDecryptionOption {
constructor(
public hasAdminApproval: boolean,
public hasLoginApprovingDevice: boolean,
public hasManageResetPasswordPermission: boolean
) {}
}

View File

@@ -0,0 +1,15 @@
import { SecretVerificationRequest } from "./secret-verification.request";
export class UpdateDevicesTrustRequest extends SecretVerificationRequest {
currentDevice: DeviceKeysUpdateRequest;
otherDevices: OtherDeviceKeysUpdateRequest[];
}
export class DeviceKeysUpdateRequest {
encryptedPublicKey: string;
encryptedUserKey: string;
}
export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest {
id: string;
}

View File

@@ -2,6 +2,7 @@ import { KdfType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
import { UserDecryptionOptionsResponse } from "./user-decryption-options/user-decryption-options.response";
export class IdentityTokenResponse extends BaseResponse {
accessToken: string;
@@ -22,6 +23,8 @@ export class IdentityTokenResponse extends BaseResponse {
apiUseKeyConnector: boolean;
keyConnectorUrl: string;
userDecryptionOptions: UserDecryptionOptionsResponse;
constructor(response: any) {
super(response);
this.accessToken = response.access_token;
@@ -43,5 +46,11 @@ export class IdentityTokenResponse extends BaseResponse {
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy")
);
if (response.UserDecryptionOptions) {
this.userDecryptionOptions = new UserDecryptionOptionsResponse(
this.getResponseProperty("UserDecryptionOptions")
);
}
}
}

View File

@@ -0,0 +1,39 @@
import { Jsonify } from "type-fest";
import { DeviceType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { EncString } from "../../../platform/models/domain/enc-string";
export class ProtectedDeviceResponse extends BaseResponse {
constructor(response: Jsonify<ProtectedDeviceResponse>) {
super(response);
this.id = this.getResponseProperty("id");
this.name = this.getResponseProperty("name");
this.identifier = this.getResponseProperty("identifier");
this.type = this.getResponseProperty("type");
this.creationDate = new Date(this.getResponseProperty("creationDate"));
if (response.encryptedUserKey) {
this.encryptedUserKey = new EncString(this.getResponseProperty("encryptedUserKey"));
}
if (response.encryptedPublicKey) {
this.encryptedPublicKey = new EncString(this.getResponseProperty("encryptedPublicKey"));
}
}
id: string;
name: string;
type: DeviceType;
identifier: string;
creationDate: Date;
/**
* Intended to be the users symmetric key that is encrypted in some form, the current way to encrypt this is with
* the devices public key.
*/
encryptedUserKey: EncString;
/**
* Intended to be the public key that was generated for a device upon trust and encrypted. Currenly encrypted using
* a users symmetric key so that when trusted and unlocked a user can decrypt the public key for all their devices.
* This enabled a user to rotate the keys for all of their devices.
*/
encryptedPublicKey: EncString;
}

View File

@@ -0,0 +1,14 @@
import { BaseResponse } from "../../../../models/response/base.response";
export interface IKeyConnectorUserDecryptionOptionServerResponse {
KeyConnectorUrl: string;
}
export class KeyConnectorUserDecryptionOptionResponse extends BaseResponse {
keyConnectorUrl: string;
constructor(response: IKeyConnectorUserDecryptionOptionServerResponse) {
super(response);
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
}
}

View File

@@ -0,0 +1,35 @@
import { BaseResponse } from "../../../../models/response/base.response";
import { EncString } from "../../../../platform/models/domain/enc-string";
export interface ITrustedDeviceUserDecryptionOptionServerResponse {
HasAdminApproval: boolean;
HasLoginApprovingDevice: boolean;
HasManageResetPasswordPermission: boolean;
EncryptedPrivateKey?: string;
EncryptedUserKey?: string;
}
export class TrustedDeviceUserDecryptionOptionResponse extends BaseResponse {
hasAdminApproval: boolean;
hasLoginApprovingDevice: boolean;
hasManageResetPasswordPermission: boolean;
encryptedPrivateKey: EncString;
encryptedUserKey: EncString;
constructor(response: any) {
super(response);
this.hasAdminApproval = this.getResponseProperty("HasAdminApproval");
this.hasLoginApprovingDevice = this.getResponseProperty("HasLoginApprovingDevice");
this.hasManageResetPasswordPermission = this.getResponseProperty(
"HasManageResetPasswordPermission"
);
if (response.EncryptedPrivateKey) {
this.encryptedPrivateKey = new EncString(this.getResponseProperty("EncryptedPrivateKey"));
}
if (response.EncryptedUserKey) {
this.encryptedUserKey = new EncString(this.getResponseProperty("EncryptedUserKey"));
}
}
}

View File

@@ -0,0 +1,39 @@
import { BaseResponse } from "../../../../models/response/base.response";
import {
IKeyConnectorUserDecryptionOptionServerResponse,
KeyConnectorUserDecryptionOptionResponse,
} from "./key-connector-user-decryption-option.response";
import {
ITrustedDeviceUserDecryptionOptionServerResponse,
TrustedDeviceUserDecryptionOptionResponse,
} from "./trusted-device-user-decryption-option.response";
export interface IUserDecryptionOptionsServerResponse {
HasMasterPassword: boolean;
TrustedDeviceOption?: ITrustedDeviceUserDecryptionOptionServerResponse;
KeyConnectorOption?: IKeyConnectorUserDecryptionOptionServerResponse;
}
export class UserDecryptionOptionsResponse extends BaseResponse {
hasMasterPassword: boolean;
trustedDeviceOption?: TrustedDeviceUserDecryptionOptionResponse;
keyConnectorOption?: KeyConnectorUserDecryptionOptionResponse;
constructor(response: IUserDecryptionOptionsServerResponse) {
super(response);
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
if (response.TrustedDeviceOption) {
this.trustedDeviceOption = new TrustedDeviceUserDecryptionOptionResponse(
this.getResponseProperty("TrustedDeviceOption")
);
}
if (response.KeyConnectorOption) {
this.keyConnectorOption = new KeyConnectorUserDecryptionOptionResponse(
this.getResponseProperty("KeyConnectorOption")
);
}
}
}

View File

@@ -0,0 +1,81 @@
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { Utils } from "../../platform/misc/utils";
import {
UserKey,
SymmetricCryptoKey,
MasterKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthRequestResponse } from "../models/response/auth-request.response";
export class AuthRequestCryptoServiceImplementation implements AuthRequestCryptoServiceAbstraction {
constructor(private cryptoService: CryptoService) {}
async setUserKeyAfterDecryptingSharedUserKey(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: Uint8Array
) {
const userKey = await this.decryptPubKeyEncryptedUserKey(
authReqResponse.key,
authReqPrivateKey
);
await this.cryptoService.setUserKey(userKey);
}
async setKeysAfterDecryptingSharedMasterKeyAndHash(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: Uint8Array
) {
const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash(
authReqResponse.key,
authReqResponse.masterPasswordHash,
authReqPrivateKey
);
// Decrypt and set user key in state
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
await this.cryptoService.setMasterKey(masterKey);
await this.cryptoService.setMasterKeyHash(masterKeyHash);
await this.cryptoService.setUserKey(userKey);
}
// Decryption helpers
async decryptPubKeyEncryptedUserKey(
pubKeyEncryptedUserKey: string,
privateKey: Uint8Array
): Promise<UserKey> {
const decryptedUserKeyBytes = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedUserKey,
privateKey
);
return new SymmetricCryptoKey(decryptedUserKeyBytes) as UserKey;
}
async decryptPubKeyEncryptedMasterKeyAndHash(
pubKeyEncryptedMasterKey: string,
pubKeyEncryptedMasterKeyHash: string,
privateKey: Uint8Array
): Promise<{ masterKey: MasterKey; masterKeyHash: string }> {
const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKey,
privateKey
);
const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKeyHash,
privateKey
);
const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey;
const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer);
return {
masterKey,
masterKeyHash,
};
}
}

View File

@@ -0,0 +1,165 @@
import { mock } from "jest-mock-extended";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { Utils } from "../../platform/misc/utils";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthRequestResponse } from "../models/response/auth-request.response";
import { AuthRequestCryptoServiceImplementation } from "./auth-request-crypto.service.implementation";
describe("AuthRequestCryptoService", () => {
let authReqCryptoService: AuthRequestCryptoServiceAbstraction;
const cryptoService = mock<CryptoService>();
let mockPrivateKey: Uint8Array;
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
authReqCryptoService = new AuthRequestCryptoServiceImplementation(cryptoService);
mockPrivateKey = new Uint8Array(64);
});
it("instantiates", () => {
expect(authReqCryptoService).not.toBeFalsy();
});
describe("setUserKeyAfterDecryptingSharedUserKey", () => {
it("decrypts and sets user key when given valid auth request response and private key", async () => {
// Arrange
const mockAuthReqResponse = {
key: "authReqPublicKeyEncryptedUserKey",
} as AuthRequestResponse;
const mockDecryptedUserKey = {} as UserKey;
jest
.spyOn(authReqCryptoService, "decryptPubKeyEncryptedUserKey")
.mockResolvedValueOnce(mockDecryptedUserKey);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
await authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
mockAuthReqResponse,
mockPrivateKey
);
// Assert
expect(authReqCryptoService.decryptPubKeyEncryptedUserKey).toBeCalledWith(
mockAuthReqResponse.key,
mockPrivateKey
);
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
});
});
describe("setKeysAfterDecryptingSharedMasterKeyAndHash", () => {
it("decrypts and sets master key and hash and user key when given valid auth request response and private key", async () => {
// Arrange
const mockAuthReqResponse = {
key: "authReqPublicKeyEncryptedMasterKey",
masterPasswordHash: "authReqPublicKeyEncryptedMasterKeyHash",
} as AuthRequestResponse;
const mockDecryptedMasterKey = {} as MasterKey;
const mockDecryptedMasterKeyHash = "mockDecryptedMasterKeyHash";
const mockDecryptedUserKey = {} as UserKey;
jest
.spyOn(authReqCryptoService, "decryptPubKeyEncryptedMasterKeyAndHash")
.mockResolvedValueOnce({
masterKey: mockDecryptedMasterKey,
masterKeyHash: mockDecryptedMasterKeyHash,
});
cryptoService.setMasterKey.mockResolvedValueOnce(undefined);
cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
await authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
mockAuthReqResponse,
mockPrivateKey
);
// Assert
expect(authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith(
mockAuthReqResponse.key,
mockAuthReqResponse.masterPasswordHash,
mockPrivateKey
);
expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey);
expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash);
expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey);
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
});
});
describe("decryptAuthReqPubKeyEncryptedUserKey", () => {
it("returns a decrypted user key when given valid public key encrypted user key and an auth req private key", async () => {
// Arrange
const mockPubKeyEncryptedUserKey = "pubKeyEncryptedUserKey";
const mockDecryptedUserKeyBytes = new Uint8Array(64);
const mockDecryptedUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes) as UserKey;
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes);
// Act
const result = await authReqCryptoService.decryptPubKeyEncryptedUserKey(
mockPubKeyEncryptedUserKey,
mockPrivateKey
);
// Assert
expect(cryptoService.rsaDecrypt).toBeCalledWith(mockPubKeyEncryptedUserKey, mockPrivateKey);
expect(result).toEqual(mockDecryptedUserKey);
});
});
describe("decryptAuthReqPubKeyEncryptedMasterKeyAndHash", () => {
it("returns a decrypted master key and hash when given a valid public key encrypted master key, public key encrypted master key hash, and an auth req private key", async () => {
// Arrange
const mockPubKeyEncryptedMasterKey = "pubKeyEncryptedMasterKey";
const mockPubKeyEncryptedMasterKeyHash = "pubKeyEncryptedMasterKeyHash";
const mockDecryptedMasterKeyBytes = new Uint8Array(64);
const mockDecryptedMasterKey = new SymmetricCryptoKey(
mockDecryptedMasterKeyBytes
) as MasterKey;
const mockDecryptedMasterKeyHashBytes = new Uint8Array(64);
const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(mockDecryptedMasterKeyHashBytes);
cryptoService.rsaDecrypt
.mockResolvedValueOnce(mockDecryptedMasterKeyBytes)
.mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes);
// Act
const result = await authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash(
mockPubKeyEncryptedMasterKey,
mockPubKeyEncryptedMasterKeyHash,
mockPrivateKey
);
// Assert
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
1,
mockPubKeyEncryptedMasterKey,
mockPrivateKey
);
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
2,
mockPubKeyEncryptedMasterKeyHash,
mockPrivateKey
);
expect(result.masterKey).toEqual(mockDecryptedMasterKey);
expect(result.masterKeyHash).toEqual(mockDecryptedMasterKeyHash);
});
});
});

View File

@@ -16,9 +16,11 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -103,7 +105,9 @@ export class AuthService implements AuthServiceAbstraction {
protected i18nService: I18nService,
protected encryptService: EncryptService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected policyService: PolicyService
protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authReqCryptoService: AuthRequestCryptoServiceAbstraction
) {}
async logIn(
@@ -149,7 +153,10 @@ export class AuthService implements AuthServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService
this.keyConnectorService,
this.deviceTrustCryptoService,
this.authReqCryptoService,
this.i18nService
);
break;
case AuthenticationType.UserApi:
@@ -178,7 +185,7 @@ export class AuthService implements AuthServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this
this.deviceTrustCryptoService
);
break;
}
@@ -238,23 +245,32 @@ export class AuthService implements AuthServiceAbstraction {
}
async getAuthStatus(userId?: string): Promise<AuthenticationStatus> {
// If we don't have an access token or userId, we're logged out
const isAuthenticated = await this.stateService.getIsAuthenticated({ userId: userId });
if (!isAuthenticated) {
return AuthenticationStatus.LoggedOut;
}
// Keys aren't stored for a device that is locked or logged out
// Make sure we're logged in before checking this, otherwise we could mix up those states
const neverLock =
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
if (neverLock) {
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
// We should refactor here.
await this.cryptoService.getKey(KeySuffixOptions.Auto, userId);
// If we don't have a user key in memory, we're locked
if (!(await this.cryptoService.hasUserKeyInMemory(userId))) {
// Check if the user has vault timeout set to never and verify that
// they've never unlocked their vault
const neverLock =
(await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto, userId)) &&
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
if (neverLock) {
// Attempt to get the key from storage and set it in memory
const userKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Auto,
userId
);
await this.cryptoService.setUserKey(userKey, userId);
}
}
const hasKeyInMemory = await this.cryptoService.hasKeyInMemory(userId);
// We do another check here in case setting the auto key failed
const hasKeyInMemory = await this.cryptoService.hasUserKeyInMemory(userId);
if (!hasKeyInMemory) {
return AuthenticationStatus.Locked;
}
@@ -262,7 +278,7 @@ export class AuthService implements AuthServiceAbstraction {
return AuthenticationStatus.Unlocked;
}
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
email = email.trim().toLowerCase();
let kdf: KdfType = null;
let kdfConfig: KdfConfig = null;
@@ -281,7 +297,7 @@ export class AuthService implements AuthServiceAbstraction {
throw e;
}
}
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfConfig);
return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
}
async authResponsePushNotification(notification: AuthRequestPushNotification): Promise<any> {
@@ -298,22 +314,33 @@ export class AuthService implements AuthServiceAbstraction {
requestApproved: boolean
): Promise<AuthRequestResponse> {
const pubKey = Utils.fromB64ToArray(key);
const encryptedKey = await this.cryptoService.rsaEncrypt(
(
await this.cryptoService.getKey()
).encKey,
pubKey
);
let encryptedMasterPassword = null;
if ((await this.stateService.getKeyHash()) != null) {
encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
pubKey
);
const masterKey = await this.cryptoService.getMasterKey();
let keyToEncrypt;
let encryptedMasterKeyHash = null;
if (masterKey) {
keyToEncrypt = masterKey.encKey;
// Only encrypt the master password hash if masterKey exists as
// we won't have a masterKeyHash without a masterKey
const masterKeyHash = await this.stateService.getKeyHash();
if (masterKeyHash != null) {
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(masterKeyHash),
pubKey
);
}
} else {
const userKey = await this.cryptoService.getUserKey();
keyToEncrypt = userKey.key;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
const request = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterPassword?.encryptedString,
encryptedMasterKeyHash?.encryptedString,
await this.appIdService.getAppId(),
requestApproved
);

View File

@@ -0,0 +1,215 @@
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import {
SymmetricCryptoKey,
DeviceKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
import {
DeviceKeysUpdateRequest,
UpdateDevicesTrustRequest,
} from "../models/request/update-devices-trust.request";
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
constructor(
private cryptoFunctionService: CryptoFunctionService,
private cryptoService: CryptoService,
private encryptService: EncryptService,
private stateService: StateService,
private appIdService: AppIdService,
private devicesApiService: DevicesApiServiceAbstraction,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService
) {}
/**
* @description Retrieves the users choice to trust the device which can only happen after decryption
* Note: this value should only be used once and then reset
*/
async getShouldTrustDevice(): Promise<boolean> {
return await this.stateService.getShouldTrustDevice();
}
async setShouldTrustDevice(value: boolean): Promise<void> {
await this.stateService.setShouldTrustDevice(value);
}
async trustDeviceIfRequired(): Promise<void> {
const shouldTrustDevice = await this.getShouldTrustDevice();
if (shouldTrustDevice) {
await this.trustDevice();
// reset the trust choice
await this.setShouldTrustDevice(false);
}
}
async trustDevice(): Promise<DeviceResponse> {
// Attempt to get user key
const userKey: UserKey = await this.cryptoService.getUserKey();
// If user key is not found, throw error
if (!userKey) {
throw new Error("User symmetric key not found");
}
// Generate deviceKey
const deviceKey = await this.makeDeviceKey();
// Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey
const [devicePublicKey, devicePrivateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(
2048
);
const [
devicePublicKeyEncryptedUserKey,
userKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey,
] = await Promise.all([
// Encrypt user key with the DevicePublicKey
this.cryptoService.rsaEncrypt(userKey.key, devicePublicKey),
// Encrypt devicePublicKey with user key
this.encryptService.encrypt(devicePublicKey, userKey),
// Encrypt devicePrivateKey with deviceKey
this.encryptService.encrypt(devicePrivateKey, deviceKey),
]);
// Send encrypted keys to server
const deviceIdentifier = await this.appIdService.getAppId();
const deviceResponse = await this.devicesApiService.updateTrustedDeviceKeys(
deviceIdentifier,
devicePublicKeyEncryptedUserKey.encryptedString,
userKeyEncryptedDevicePublicKey.encryptedString,
deviceKeyEncryptedDevicePrivateKey.encryptedString
);
// store device key in local/secure storage if enc keys posted to server successfully
await this.setDeviceKey(deviceKey);
this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted"));
return deviceResponse;
}
async rotateDevicesTrust(newUserKey: UserKey, masterPasswordHash: string): Promise<void> {
const currentDeviceKey = await this.getDeviceKey();
if (currentDeviceKey == null) {
// If the current device doesn't have a device key available to it, then we can't
// rotate any trust at all, so early return.
return;
}
// At this point of rotating their keys, they should still have their old user key in state
const oldUserKey = await this.stateService.getUserKey();
const deviceIdentifier = await this.appIdService.getAppId();
const secretVerificationRequest = new SecretVerificationRequest();
secretVerificationRequest.masterPasswordHash = masterPasswordHash;
// Get the keys that are used in rotating a devices keys from the server
const currentDeviceKeys = await this.devicesApiService.getDeviceKeys(
deviceIdentifier,
secretVerificationRequest
);
// Decrypt the existing device public key with the old user key
const decryptedDevicePublicKey = await this.encryptService.decryptToBytes(
currentDeviceKeys.encryptedPublicKey,
oldUserKey
);
// Encrypt the brand new user key with the now-decrypted public key for the device
const encryptedNewUserKey = await this.cryptoService.rsaEncrypt(
newUserKey.key,
decryptedDevicePublicKey
);
// Re-encrypt the device public key with the new user key
const encryptedDevicePublicKey = await this.encryptService.encrypt(
decryptedDevicePublicKey,
newUserKey
);
const currentDeviceUpdateRequest = new DeviceKeysUpdateRequest();
currentDeviceUpdateRequest.encryptedUserKey = encryptedNewUserKey.encryptedString;
currentDeviceUpdateRequest.encryptedPublicKey = encryptedDevicePublicKey.encryptedString;
// TODO: For device management, allow this method to take an array of device ids that can be looped over and individually rotated
// then it can be added to trustRequest.otherDevices.
const trustRequest = new UpdateDevicesTrustRequest();
trustRequest.masterPasswordHash = masterPasswordHash;
trustRequest.currentDevice = currentDeviceUpdateRequest;
trustRequest.otherDevices = [];
await this.devicesApiService.updateTrust(trustRequest);
}
async getDeviceKey(): Promise<DeviceKey> {
return await this.stateService.getDeviceKey();
}
private async setDeviceKey(deviceKey: DeviceKey | null): Promise<void> {
await this.stateService.setDeviceKey(deviceKey);
}
private async makeDeviceKey(): Promise<DeviceKey> {
// Create 512-bit device key
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
return deviceKey;
}
async decryptUserKeyWithDeviceKey(
encryptedDevicePrivateKey: EncString,
encryptedUserKey: EncString,
deviceKey?: DeviceKey
): Promise<UserKey | null> {
// If device key provided use it, otherwise try to retrieve from storage
deviceKey ||= await this.getDeviceKey();
if (!deviceKey) {
// User doesn't have a device key anymore so device is untrusted
return null;
}
try {
// attempt to decrypt encryptedDevicePrivateKey with device key
const devicePrivateKey = await this.encryptService.decryptToBytes(
encryptedDevicePrivateKey,
deviceKey
);
// Attempt to decrypt encryptedUserDataKey with devicePrivateKey
const userKey = await this.cryptoService.rsaDecrypt(
encryptedUserKey.encryptedString,
devicePrivateKey
);
return new SymmetricCryptoKey(userKey) as UserKey;
} catch (e) {
// If either decryption effort fails, we want to remove the device key
await this.setDeviceKey(null);
return null;
}
}
async supportsDeviceTrust(): Promise<boolean> {
const decryptionOptions = await this.stateService.getAccountDecryptionOptions();
return decryptionOptions?.trustedDeviceOption != null;
}
}

View File

@@ -0,0 +1,598 @@
import { matches, mock } from "jest-mock-extended";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { DeviceType } from "../../enums";
import { EncryptionType } from "../../enums/encryption-type.enum";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import {
SymmetricCryptoKey,
DeviceKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation";
describe("deviceTrustCryptoService", () => {
let deviceTrustCryptoService: DeviceTrustCryptoService;
const cryptoFunctionService = mock<CryptoFunctionService>();
const cryptoService = mock<CryptoService>();
const encryptService = mock<EncryptService>();
const stateService = mock<StateService>();
const appIdService = mock<AppIdService>();
const devicesApiService = mock<DevicesApiServiceAbstraction>();
const i18nService = mock<I18nService>();
const platformUtilsService = mock<PlatformUtilsService>();
beforeEach(() => {
jest.clearAllMocks();
deviceTrustCryptoService = new DeviceTrustCryptoService(
cryptoFunctionService,
cryptoService,
encryptService,
stateService,
appIdService,
devicesApiService,
i18nService,
platformUtilsService
);
});
it("instantiates", () => {
expect(deviceTrustCryptoService).not.toBeFalsy();
});
describe("User Trust Device Choice For Decryption", () => {
describe("getShouldTrustDevice", () => {
it("gets the user trust device choice for decryption from the state service", async () => {
const stateSvcGetShouldTrustDeviceSpy = jest.spyOn(stateService, "getShouldTrustDevice");
const expectedValue = true;
stateSvcGetShouldTrustDeviceSpy.mockResolvedValue(expectedValue);
const result = await deviceTrustCryptoService.getShouldTrustDevice();
expect(stateSvcGetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
expect(result).toEqual(expectedValue);
});
});
describe("setShouldTrustDevice", () => {
it("sets the user trust device choice for decryption in the state service", async () => {
const stateSvcSetShouldTrustDeviceSpy = jest.spyOn(stateService, "setShouldTrustDevice");
const newValue = true;
await deviceTrustCryptoService.setShouldTrustDevice(newValue);
expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledWith(newValue);
});
});
});
describe("trustDeviceIfRequired", () => {
it("should trust device and reset when getShouldTrustDevice returns true", async () => {
jest.spyOn(deviceTrustCryptoService, "getShouldTrustDevice").mockResolvedValue(true);
jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse);
jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue();
await deviceTrustCryptoService.trustDeviceIfRequired();
expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1);
expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1);
expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(false);
});
it("should not trust device nor reset when getShouldTrustDevice returns false", async () => {
const getShouldTrustDeviceSpy = jest
.spyOn(deviceTrustCryptoService, "getShouldTrustDevice")
.mockResolvedValue(false);
const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice");
const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice");
await deviceTrustCryptoService.trustDeviceIfRequired();
expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
expect(trustDeviceSpy).not.toHaveBeenCalled();
expect(setShouldTrustDeviceSpy).not.toHaveBeenCalled();
});
});
describe("Trusted Device Encryption core logic tests", () => {
const deviceKeyBytesLength = 64;
const userKeyBytesLength = 64;
describe("getDeviceKey", () => {
let existingDeviceKey: DeviceKey;
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
beforeEach(() => {
existingDeviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength) as CsprngArray
) as DeviceKey;
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
});
it("returns null when there is not an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
const deviceKey = await deviceTrustCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(deviceKey).toBeNull();
});
it("returns the device key when there is an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
const deviceKey = await deviceTrustCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(deviceKey).toEqual(existingDeviceKey);
});
});
describe("setDeviceKey", () => {
it("sets the device key in the state service", async () => {
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
const deviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength) as CsprngArray
) as DeviceKey;
// TypeScript will allow calling private methods if the object is of type 'any'
// This is a hacky workaround, but it allows for cleaner tests
await (deviceTrustCryptoService as any).setDeviceKey(deviceKey);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
});
});
describe("makeDeviceKey", () => {
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
const cryptoFuncSvcRandomBytesSpy = jest
.spyOn(cryptoFunctionService, "randomBytes")
.mockResolvedValue(mockRandomBytes);
// TypeScript will allow calling private methods if the object is of type 'any'
// This is a hacky workaround, but it allows for cleaner tests
const deviceKey = await (deviceTrustCryptoService as any).makeDeviceKey();
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
});
});
describe("trustDevice", () => {
let mockDeviceKeyRandomBytes: CsprngArray;
let mockDeviceKey: DeviceKey;
let mockUserKeyRandomBytes: CsprngArray;
let mockUserKey: UserKey;
const deviceRsaKeyLength = 2048;
let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array];
let mockDevicePrivateKey: Uint8Array;
let mockDevicePublicKey: Uint8Array;
let mockDevicePublicKeyEncryptedUserKey: EncString;
let mockUserKeyEncryptedDevicePublicKey: EncString;
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
Id: "mockId",
Name: "mockName",
Identifier: "mockIdentifier",
Type: "mockType",
CreationDate: "mockCreationDate",
});
const mockDeviceId = "mockDeviceId";
let makeDeviceKeySpy: jest.SpyInstance;
let rsaGenerateKeyPairSpy: jest.SpyInstance;
let cryptoSvcGetUserKeySpy: jest.SpyInstance;
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
let encryptServiceEncryptSpy: jest.SpyInstance;
let appIdServiceGetAppIdSpy: jest.SpyInstance;
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
beforeEach(() => {
// Setup all spies and default return values for the happy path
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
mockDeviceRsaKeyPair = [
new Uint8Array(deviceRsaKeyLength),
new Uint8Array(deviceRsaKeyLength),
];
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
mockDevicePublicKeyEncryptedUserKey = new EncString(
EncryptionType.Rsa2048_OaepSha1_B64,
"mockDevicePublicKeyEncryptedUserKey"
);
mockUserKeyEncryptedDevicePublicKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockUserKeyEncryptedDevicePublicKey"
);
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockDeviceKeyEncryptedDevicePrivateKey"
);
// TypeScript will allow calling private methods if the object is of type 'any'
makeDeviceKeySpy = jest
.spyOn(deviceTrustCryptoService as any, "makeDeviceKey")
.mockResolvedValue(mockDeviceKey);
rsaGenerateKeyPairSpy = jest
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
.mockResolvedValue(mockDeviceRsaKeyPair);
cryptoSvcGetUserKeySpy = jest
.spyOn(cryptoService, "getUserKey")
.mockResolvedValue(mockUserKey);
cryptoSvcRsaEncryptSpy = jest
.spyOn(cryptoService, "rsaEncrypt")
.mockResolvedValue(mockDevicePublicKeyEncryptedUserKey);
encryptServiceEncryptSpy = jest
.spyOn(encryptService, "encrypt")
.mockImplementation((plainValue, key) => {
if (plainValue === mockDevicePublicKey && key === mockUserKey) {
return Promise.resolve(mockUserKeyEncryptedDevicePublicKey);
}
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
}
});
appIdServiceGetAppIdSpy = jest
.spyOn(appIdService, "getAppId")
.mockResolvedValue(mockDeviceId);
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
.mockResolvedValue(mockDeviceResponse);
});
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
const response = await deviceTrustCryptoService.trustDevice();
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcGetUserKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
// RsaEncrypt must be called w/ a user key array buffer of 64 bytes
const userKeyKey: Uint8Array = cryptoSvcRsaEncryptSpy.mock.calls[0][0];
expect(userKeyKey.byteLength).toBe(64);
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
mockDeviceId,
mockDevicePublicKeyEncryptedUserKey.encryptedString,
mockUserKeyEncryptedDevicePublicKey.encryptedString,
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
);
expect(response).toBeInstanceOf(DeviceResponse);
expect(response).toEqual(mockDeviceResponse);
});
it("throws specific error if user key is not found", async () => {
// setup the spy to return null
cryptoSvcGetUserKeySpy.mockResolvedValue(null);
// check if the expected error is thrown
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
// reset the spy
cryptoSvcGetUserKeySpy.mockReset();
// setup the spy to return undefined
cryptoSvcGetUserKeySpy.mockResolvedValue(undefined);
// check if the expected error is thrown
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
});
const methodsToTestForErrorsOrInvalidReturns: any = [
{
method: "makeDeviceKey",
spy: () => makeDeviceKeySpy,
errorText: "makeDeviceKey error",
},
{
method: "rsaGenerateKeyPair",
spy: () => rsaGenerateKeyPairSpy,
errorText: "rsaGenerateKeyPair error",
},
{
method: "getUserKey",
spy: () => cryptoSvcGetUserKeySpy,
errorText: "getUserKey error",
},
{
method: "rsaEncrypt",
spy: () => cryptoSvcRsaEncryptSpy,
errorText: "rsaEncrypt error",
},
{
method: "encryptService.encrypt",
spy: () => encryptServiceEncryptSpy,
errorText: "encryptService.encrypt error",
},
];
describe.each(methodsToTestForErrorsOrInvalidReturns)(
"trustDevice error handling and invalid return testing",
({ method, spy, errorText }) => {
// ensures that error propagation works correctly
it(`throws an error if ${method} fails`, async () => {
const methodSpy = spy();
methodSpy.mockRejectedValue(new Error(errorText));
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(errorText);
});
test.each([null, undefined])(
`throws an error if ${method} returns %s`,
async (invalidValue) => {
const methodSpy = spy();
methodSpy.mockResolvedValue(invalidValue);
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow();
}
);
}
);
});
describe("decryptUserKeyWithDeviceKey", () => {
let mockDeviceKey: DeviceKey;
let mockEncryptedDevicePrivateKey: EncString;
let mockEncryptedUserKey: EncString;
let mockUserKey: UserKey;
beforeEach(() => {
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
const mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
mockEncryptedDevicePrivateKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockEncryptedDevicePrivateKey"
);
mockEncryptedUserKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockEncryptedUserKey"
);
jest.clearAllMocks();
});
it("returns null when device key isn't provided and isn't in state", async () => {
const getDeviceKeySpy = jest
.spyOn(deviceTrustCryptoService, "getDeviceKey")
.mockResolvedValue(null);
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
mockEncryptedDevicePrivateKey,
mockEncryptedUserKey
);
expect(result).toBeNull();
expect(getDeviceKeySpy).toHaveBeenCalledTimes(1);
});
it("successfully returns the user key when provided keys (including device key) can decrypt it", async () => {
const decryptToBytesSpy = jest
.spyOn(encryptService, "decryptToBytes")
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
const rsaDecryptSpy = jest
.spyOn(cryptoService, "rsaDecrypt")
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
mockEncryptedDevicePrivateKey,
mockEncryptedUserKey,
mockDeviceKey
);
expect(result).toEqual(mockUserKey);
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
});
it("successfully returns the user key when a device key is not provided (retrieves device key from state)", async () => {
const getDeviceKeySpy = jest
.spyOn(deviceTrustCryptoService, "getDeviceKey")
.mockResolvedValue(mockDeviceKey);
const decryptToBytesSpy = jest
.spyOn(encryptService, "decryptToBytes")
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
const rsaDecryptSpy = jest
.spyOn(cryptoService, "rsaDecrypt")
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
// Call without providing a device key
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
mockEncryptedDevicePrivateKey,
mockEncryptedUserKey
);
expect(getDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockUserKey);
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
});
it("returns null and removes device key when the decryption fails", async () => {
const decryptToBytesSpy = jest
.spyOn(encryptService, "decryptToBytes")
.mockRejectedValue(new Error("Decryption error"));
const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey");
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
mockEncryptedDevicePrivateKey,
mockEncryptedUserKey,
mockDeviceKey
);
expect(result).toBeNull();
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
expect(setDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(setDeviceKeySpy).toHaveBeenCalledWith(null);
});
});
describe("rotateDevicesTrust", () => {
let fakeNewUserKey: UserKey = null;
const FakeNewUserKeyMarker = 1;
const FakeOldUserKeyMarker = 5;
const FakeDecryptedPublicKeyMarker = 17;
beforeEach(() => {
const fakeNewUserKeyData = new Uint8Array(64);
fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1);
fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey;
});
it("does an early exit when the current device is not a trusted device", async () => {
stateService.getDeviceKey.mockResolvedValue(null);
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "");
expect(devicesApiService.updateTrust).not.toBeCalled();
});
describe("is on a trusted device", () => {
beforeEach(() => {
stateService.getDeviceKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(deviceKeyBytesLength)) as DeviceKey
);
});
it("rotates current device keys and calls api service when the current device is trusted", async () => {
const currentEncryptedPublicKey = new EncString("2.cHVibGlj|cHVibGlj|cHVibGlj");
const currentEncryptedUserKey = new EncString("4.dXNlcg==");
const fakeOldUserKeyData = new Uint8Array(new Uint8Array(64));
// Fill the first byte with something identifiable
fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1);
// Mock the retrieval of a user key that differs from the new one passed into the method
stateService.getUserKey.mockResolvedValue(
new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey
);
appIdService.getAppId.mockResolvedValue("test_device_identifier");
devicesApiService.getDeviceKeys.mockImplementation((deviceIdentifier, secretRequest) => {
if (
deviceIdentifier !== "test_device_identifier" ||
secretRequest.masterPasswordHash !== "my_password_hash"
) {
return Promise.resolve(null);
}
return Promise.resolve(
new ProtectedDeviceResponse({
id: "",
creationDate: "",
identifier: "test_device_identifier",
name: "Firefox",
type: DeviceType.FirefoxBrowser,
encryptedPublicKey: currentEncryptedPublicKey.encryptedString,
encryptedUserKey: currentEncryptedUserKey.encryptedString,
})
);
});
// Mock the decryption of the public key with the old user key
encryptService.decryptToBytes.mockImplementationOnce((_encValue, privateKeyValue) => {
expect(privateKeyValue.key.byteLength).toBe(64);
expect(new Uint8Array(privateKeyValue.key)[0]).toBe(FakeOldUserKeyMarker);
const data = new Uint8Array(250);
data.fill(FakeDecryptedPublicKeyMarker, 0, 1);
return Promise.resolve(data);
});
// Mock the encryption of the new user key with the decrypted public key
cryptoService.rsaEncrypt.mockImplementationOnce((data, publicKey) => {
expect(data.byteLength).toBe(64); // New key should also be 64 bytes
expect(new Uint8Array(data)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1';
expect(new Uint8Array(publicKey)[0]).toBe(FakeDecryptedPublicKeyMarker);
return Promise.resolve(new EncString("4.ZW5jcnlwdGVkdXNlcg=="));
});
// Mock the reencryption of the device public key with the new user key
encryptService.encrypt.mockImplementationOnce((plainValue, key) => {
expect(plainValue).toBeInstanceOf(Uint8Array);
expect(new Uint8Array(plainValue as Uint8Array)[0]).toBe(FakeDecryptedPublicKeyMarker);
expect(new Uint8Array(key.key)[0]).toBe(FakeNewUserKeyMarker);
return Promise.resolve(
new EncString("2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj")
);
});
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "my_password_hash");
expect(devicesApiService.updateTrust).toBeCalledWith(
matches((updateTrustModel: UpdateDevicesTrustRequest) => {
return (
updateTrustModel.currentDevice.encryptedPublicKey ===
"2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj" &&
updateTrustModel.currentDevice.encryptedUserKey === "4.ZW5jcnlwdGVkdXNlcg=="
);
})
);
});
});
});
});
});

View File

@@ -0,0 +1,96 @@
import { ApiService } from "../../abstractions/api.service";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { ListResponse } from "../../models/response/list.response";
import { Utils } from "../../platform/misc/utils";
import { TrustedDeviceKeysRequest } from "../../services/devices/requests/trusted-device-keys.request";
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
const r = await this.apiService.send(
"GET",
"/devices/knowndevice",
null,
false,
true,
null,
(headers) => {
headers.set("X-Device-Identifier", deviceIdentifier);
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
}
);
return r as boolean;
}
/**
* Get device by identifier
* @param deviceIdentifier - client generated id (not device id in DB)
*/
async getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse> {
const r = await this.apiService.send(
"GET",
`/devices/identifier/${deviceIdentifier}`,
null,
true,
true
);
return new DeviceResponse(r);
}
async getDevices(): Promise<ListResponse<DeviceResponse>> {
const r = await this.apiService.send("GET", "/devices", null, true, true, null);
return new ListResponse(r, DeviceResponse);
}
async updateTrustedDeviceKeys(
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
): Promise<DeviceResponse> {
const request = new TrustedDeviceKeysRequest(
devicePublicKeyEncryptedUserKey,
userKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey
);
const result = await this.apiService.send(
"PUT",
`/devices/${deviceIdentifier}/keys`,
request,
true,
true
);
return new DeviceResponse(result);
}
async updateTrust(updateDevicesTrustRequestModel: UpdateDevicesTrustRequest): Promise<void> {
await this.apiService.send(
"POST",
"/devices/update-trust",
updateDevicesTrustRequestModel,
true,
false
);
}
async getDeviceKeys(
deviceIdentifier: string,
secretVerificationRequest: SecretVerificationRequest
): Promise<ProtectedDeviceResponse> {
const result = await this.apiService.send(
"POST",
`/devices/${deviceIdentifier}/retrieve-keys`,
secretVerificationRequest,
true,
true
);
return new ProtectedDeviceResponse(result);
}
}

View File

@@ -7,7 +7,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { MasterKey, SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { KdfConfig } from "../models/domain/kdf-config";
@@ -45,8 +45,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
async migrateUser() {
const organization = await this.getManagingOrganization();
const key = await this.cryptoService.getKey();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
const masterKey = await this.cryptoService.getMasterKey();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
try {
await this.apiService.postUserKeyToKeyConnector(
@@ -60,12 +60,13 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.apiService.postConvertToKeyConnector();
}
async getAndSetKey(url: string) {
// TODO: UserKey should be renamed to MasterKey and typed accordingly
async setMasterKeyFromUrl(url: string) {
try {
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
const k = new SymmetricCryptoKey(keyArr);
await this.cryptoService.setKey(k);
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
await this.cryptoService.setMasterKey(masterKey);
} catch (e) {
this.handleKeyConnectorError(e);
}
@@ -87,17 +88,18 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
const password = await this.cryptoFunctionService.randomBytes(64);
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const k = await this.cryptoService.makeKey(
const masterKey = await this.cryptoService.makeMasterKey(
Utils.fromBufferToB64(password),
await this.tokenService.getEmail(),
kdf,
kdfConfig
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64);
await this.cryptoService.setKey(k);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
await this.cryptoService.setMasterKey(masterKey);
const encKey = await this.cryptoService.makeEncKey(k);
await this.cryptoService.setEncKey(encKey[1].encryptedString);
const userKey = await this.cryptoService.makeUserKey(masterKey);
await this.cryptoService.setUserKey(userKey[0]);
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString);
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
@@ -109,7 +111,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
const keys = new KeysRequest(pubKey, privKey.encryptedString);
const setPasswordRequest = new SetKeyConnectorKeyRequest(
encKey[1].encryptedString,
userKey[1].encryptedString,
kdf,
kdfConfig,
orgId,

View File

@@ -0,0 +1,123 @@
import { mock, MockProxy } from "jest-mock-extended";
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
describe("PasswordResetEnrollmentServiceImplementation", () => {
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let stateService: MockProxy<StateService>;
let cryptoService: MockProxy<CryptoService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let i18nService: MockProxy<I18nService>;
let service: PasswordResetEnrollmentServiceImplementation;
beforeEach(() => {
organizationApiService = mock<OrganizationApiServiceAbstraction>();
stateService = mock<StateService>();
cryptoService = mock<CryptoService>();
organizationUserService = mock<OrganizationUserService>();
i18nService = mock<I18nService>();
service = new PasswordResetEnrollmentServiceImplementation(
organizationApiService,
stateService,
cryptoService,
organizationUserService,
i18nService
);
});
describe("enrollIfRequired", () => {
it("should not enroll when user is already enrolled in password reset", async () => {
const mockResponse = new OrganizationAutoEnrollStatusResponse({
ResetPasswordEnabled: true,
Id: "orgId",
});
organizationApiService.getAutoEnrollStatus.mockResolvedValue(mockResponse);
const enrollSpy = jest.spyOn(service, "enroll");
enrollSpy.mockResolvedValue();
await service.enrollIfRequired("ssoId");
expect(service.enroll).not.toHaveBeenCalled();
});
it("should enroll when user is not enrolled in password reset", async () => {
const mockResponse = new OrganizationAutoEnrollStatusResponse({
ResetPasswordEnabled: false,
Id: "orgId",
});
organizationApiService.getAutoEnrollStatus.mockResolvedValue(mockResponse);
const enrollSpy = jest.spyOn(service, "enroll");
enrollSpy.mockResolvedValue();
await service.enrollIfRequired("ssoId");
expect(service.enroll).toHaveBeenCalled();
});
});
describe("enroll", () => {
it("should throw an error if the organization keys are not found", async () => {
organizationApiService.getKeys.mockResolvedValue(null);
i18nService.t.mockReturnValue("resetPasswordOrgKeysError");
const result = () => service.enroll("orgId");
await expect(result).rejects.toThrowError("resetPasswordOrgKeysError");
});
it("should enroll the user when no user id or key is provided", async () => {
const orgKeyResponse = {
publicKey: "publicKey",
privateKey: "privateKey",
};
const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
stateService.getUserId.mockResolvedValue("userId");
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
await service.enroll("orgId");
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).toHaveBeenCalledWith(
"orgId",
"userId",
expect.objectContaining({
resetPasswordKey: encryptedKey.encryptedString,
})
);
});
it("should enroll the user when a user id and key is provided", async () => {
const orgKeyResponse = {
publicKey: "publicKey",
privateKey: "privateKey",
};
const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
await service.enroll("orgId", "userId", { key: "key" } as any);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).toHaveBeenCalledWith(
"orgId",
"userId",
expect.objectContaining({
resetPasswordKey: encryptedKey.encryptedString,
})
);
});
});
});

View File

@@ -0,0 +1,56 @@
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../abstractions/organization-user/requests";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
export class PasswordResetEnrollmentServiceImplementation
implements PasswordResetEnrollmentServiceAbstraction
{
constructor(
protected organizationApiService: OrganizationApiServiceAbstraction,
protected stateService: StateService,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected i18nService: I18nService
) {}
async enrollIfRequired(organizationSsoIdentifier: string): Promise<void> {
const orgAutoEnrollStatusResponse = await this.organizationApiService.getAutoEnrollStatus(
organizationSsoIdentifier
);
if (!orgAutoEnrollStatusResponse.resetPasswordEnabled) {
await this.enroll(orgAutoEnrollStatusResponse.id, null, null);
}
}
async enroll(organizationId: string): Promise<void>;
async enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
async enroll(organizationId: string, userId?: string, userKey?: UserKey): Promise<void> {
const orgKeyResponse = await this.organizationApiService.getKeys(organizationId);
if (orgKeyResponse == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
userId = userId ?? (await this.stateService.getUserId());
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
// RSA Encrypt user's userKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
organizationId,
userId,
resetRequest
);
}
}

View File

@@ -1,5 +1,6 @@
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { Verification } from "../../../types/verification";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
@@ -13,6 +14,7 @@ import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
*/
export class UserVerificationService implements UserVerificationServiceAbstraction {
constructor(
private stateService: StateService,
private cryptoService: CryptoService,
private i18nService: I18nService,
private userVerificationApiService: UserVerificationApiServiceAbstraction
@@ -37,9 +39,18 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
if (verification.type === VerificationType.OTP) {
request.otp = verification.secret;
} else {
let masterKey = await this.cryptoService.getMasterKey();
if (!masterKey && !alreadyHashed) {
masterKey = await this.cryptoService.makeMasterKey(
verification.secret,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig()
);
}
request.masterPasswordHash = alreadyHashed
? verification.secret
: await this.cryptoService.hashPassword(verification.secret, null);
: await this.cryptoService.hashMasterKey(verification.secret, masterKey);
}
return request;
@@ -61,13 +72,23 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
throw new Error(this.i18nService.t("invalidVerificationCode"));
}
} else {
let masterKey = await this.cryptoService.getMasterKey();
if (!masterKey) {
masterKey = await this.cryptoService.makeMasterKey(
verification.secret,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig()
);
}
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
verification.secret,
null
masterKey
);
if (!passwordValid) {
throw new Error(this.i18nService.t("invalidMasterPassword"));
}
this.cryptoService.setMasterKey(masterKey);
}
return true;
}
@@ -76,6 +97,30 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
await this.userVerificationApiService.postAccountRequestOTP();
}
/**
* Check if user has master password or can only use passwordless technologies to log in
* Note: This only checks the server, not the local state
* @param userId The user id to check. If not provided, the current user is used
* @returns True if the user has a master password
*/
async hasMasterPassword(userId?: string): Promise<boolean> {
const decryptionOptions = await this.stateService.getAccountDecryptionOptions({ userId });
if (decryptionOptions?.hasMasterPassword != undefined) {
return decryptionOptions.hasMasterPassword;
}
// TODO: PM-3518 - Left for backwards compatibility, remove after 2023.12.0
return !(await this.stateService.getUsesKeyConnector({ userId }));
}
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {
return (
(await this.hasMasterPassword(userId)) &&
(await this.cryptoService.getMasterKeyHash()) != null
);
}
private validateInput(verification: Verification) {
if (verification?.secret == null || verification.secret === "") {
if (verification.type === VerificationType.OTP) {

View File

@@ -23,3 +23,16 @@ export enum DeviceType {
SDK = 21,
Server = 22,
}
export const MobileDeviceTypes: Set<DeviceType> = new Set([
DeviceType.Android,
DeviceType.iOS,
DeviceType.AndroidAmazon,
]);
export const DesktopDeviceTypes: Set<DeviceType> = new Set([
DeviceType.WindowsDesktop,
DeviceType.MacOsDesktop,
DeviceType.LinuxDesktop,
DeviceType.UWP,
]);

View File

@@ -10,6 +10,7 @@ export enum EventType {
User_ClientExportedVault = 1007,
User_UpdatedTempPassword = 1008,
User_MigratedKeyToKeyConnector = 1009,
User_RequestedDeviceApproval = 1010,
Cipher_Created = 1100,
Cipher_Updated = 1101,
@@ -51,6 +52,8 @@ export enum EventType {
OrganizationUser_FirstSsoLogin = 1510,
OrganizationUser_Revoked = 1511,
OrganizationUser_Restored = 1512,
OrganizationUser_ApprovedAuthRequest = 1513,
OrganizationUser_RejectedAuthRequest = 1514,
Organization_Updated = 1600,
Organization_PurgedVault = 1601,

View File

@@ -1,4 +1,5 @@
export enum KeySuffixOptions {
Auto = "auto",
Biometric = "biometric",
Pin = "pin",
}

View File

@@ -5,82 +5,420 @@ import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { KeySuffixOptions, KdfType, HashPurpose } from "../../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import {
MasterKey,
OrgKey,
PinKey,
ProviderKey,
SymmetricCryptoKey,
UserKey,
} from "../models/domain/symmetric-crypto-key";
export abstract class CryptoService {
setKey: (key: SymmetricCryptoKey) => Promise<any>;
setKeyHash: (keyHash: string) => Promise<void>;
setEncKey: (encKey: string) => Promise<void>;
setEncPrivateKey: (encPrivateKey: string) => Promise<void>;
/**
* Sets the provided user key and stores
* any other necessary versions (such as auto, biometrics,
* or pin)
* @param key The user key to set
* @param userId The desired user
*/
setUserKey: (key: UserKey, userId?: string) => Promise<void>;
/**
* Gets the user key from memory and sets it again,
* kicking off a refresh of any additional keys
* (such as auto, biometrics, or pin)
*/
/**
* Check if the current sessions has ever had a user key, i.e. has ever been unlocked/decrypted.
* This is key for differentiating between TDE locked and standard locked states.
* @param userId The desired user
* @returns True if the current session has ever had a user key
*/
getEverHadUserKey: (userId?: string) => Promise<boolean>;
refreshAdditionalKeys: () => Promise<void>;
/**
* Retrieves the user key
* @param userId The desired user
* @returns The user key
*/
getUserKey: (userId?: string) => 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
*/
getUserKeyWithLegacySupport: (userId?: string) => Promise<UserKey>;
/**
* Retrieves the user key from storage
* @param keySuffix The desired version of the user's key to retrieve
* @param userId The desired user
* @returns The user key
*/
getUserKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<UserKey>;
/**
* @returns True if the user key is available
*/
hasUserKey: () => Promise<boolean>;
/**
* @param userId The desired user
* @returns True if the user key is set in memory
*/
hasUserKeyInMemory: (userId?: string) => Promise<boolean>;
/**
* @param keySuffix The desired version of the user's key to check
* @param userId The desired user
* @returns True if the provided version of the user key is stored
*/
hasUserKeyStored: (keySuffix: KeySuffixOptions, userId?: string) => Promise<boolean>;
/**
* Generates a new user key
* @param masterKey The user's master key
* @returns A new user key and the master key protected version of it
*/
makeUserKey: (key: MasterKey) => Promise<[UserKey, EncString]>;
/**
* Clears the user key
* @param clearStoredKeys Clears all stored versions of the user keys as well,
* such as the biometrics key
* @param userId The desired user
*/
clearUserKey: (clearSecretStorage?: boolean, userId?: string) => Promise<void>;
/**
* Clears the user's stored version of the user key
* @param keySuffix The desired version of the key to clear
* @param userId The desired user
*/
clearStoredUserKey: (keySuffix: KeySuffixOptions, userId?: string) => Promise<void>;
/**
* Stores the master key encrypted user key
* @param userKeyMasterKey The master key encrypted user key to set
* @param userId The desired user
*/
setMasterKeyEncryptedUserKey: (UserKeyMasterKey: string, userId?: string) => Promise<void>;
/**
* Sets the user's master key
* @param key The user's master key to set
* @param userId The desired user
*/
setMasterKey: (key: MasterKey, userId?: string) => Promise<void>;
/**
* @param userId The desired user
* @returns The user's master key
*/
getMasterKey: (userId?: string) => Promise<MasterKey>;
/**
* @param password The user's master password that will be used to derive a master key if one isn't found
* @param userId The desired user
*/
getOrDeriveMasterKey: (password: string, userId?: string) => Promise<MasterKey>;
/**
* Generates a master key from the provided password
* @param password The user's master password
* @param email The user's email
* @param kdf The user's selected key derivation function to use
* @param KdfConfig The user's key derivation function configuration
* @returns A master key derived from the provided password
*/
makeMasterKey: (
password: string,
email: string,
kdf: KdfType,
KdfConfig: KdfConfig
) => Promise<MasterKey>;
/**
* Clears the user's master key
* @param userId The desired user
*/
clearMasterKey: (userId?: string) => Promise<void>;
/**
* Encrypts the existing (or provided) user key with the
* provided master key
* @param masterKey The user's master key
* @param userKey The user key
* @returns The user key and the master key protected version of it
*/
encryptUserKeyWithMasterKey: (
masterKey: MasterKey,
userKey?: UserKey
) => Promise<[UserKey, EncString]>;
/**
* Decrypts the user key with the provided master key
* @param masterKey The user's master key
* @param userKey The user's encrypted symmetric key
* @param userId The desired user
* @returns The user key
*/
decryptUserKeyWithMasterKey: (
masterKey: MasterKey,
userKey?: EncString,
userId?: string
) => Promise<UserKey>;
/**
* Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending
* on the hashPurpose provided.
* @param password The user's master password
* @param key The user's master key
* @param hashPurpose The iterations to use for the hash
* @returns The user's master password hash
*/
hashMasterKey: (password: string, key: MasterKey, hashPurpose?: HashPurpose) => Promise<string>;
/**
* Sets the user's master password hash
* @param keyHash The user's master password hash to set
*/
setMasterKeyHash: (keyHash: string) => Promise<void>;
/**
* @returns The user's master password hash
*/
getMasterKeyHash: () => Promise<string>;
/**
* Clears the user's stored master password hash
* @param userId The desired user
*/
clearMasterKeyHash: (userId?: string) => Promise<void>;
/**
* Compares the provided master password to the stored password hash and server password hash.
* Updates the stored hash if outdated.
* @param masterPassword The user's master password
* @param key The user's master key
* @returns True if the provided master password matches either the stored
* key hash or the server key hash
*/
compareAndUpdateKeyHash: (masterPassword: string, masterKey: MasterKey) => Promise<boolean>;
/**
* Stores the encrypted organization keys and clears any decrypted
* organization keys currently in memory
* @param orgs The organizations to set keys for
* @param providerOrgs The provider organizations to set keys for
*/
setOrgKeys: (
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[]
) => Promise<void>;
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
getKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
getKeyHash: () => Promise<string>;
compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise<boolean>;
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
getPublicKey: () => Promise<Uint8Array>;
getPrivateKey: () => Promise<Uint8Array>;
getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise<string[]>;
/**
* Returns the organization's symmetric key
* @param orgId The desired organization
* @returns The organization's symmetric key
*/
getOrgKey: (orgId: string) => Promise<OrgKey>;
/**
* @returns A map of the organization Ids to their symmetric keys
*/
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
getKeyForUserEncryption: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
hasKey: () => Promise<boolean>;
hasKeyInMemory: (userId?: string) => Promise<boolean>;
hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<boolean>;
hasEncKey: () => Promise<boolean>;
clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise<any>;
clearKeyHash: () => Promise<any>;
clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearProviderKeys: (memoryOnly?: boolean) => Promise<any>;
clearPinProtectedKey: () => Promise<any>;
clearKeys: (userId?: string) => Promise<any>;
toggleKey: () => Promise<any>;
makeKey: (
password: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig
) => Promise<SymmetricCryptoKey>;
makeKeyFromPin: (
/**
* Uses the org key to derive a new symmetric key for encrypting data
* @param orgKey The organization's symmetric key
*/
makeDataEncKey: <T extends UserKey | OrgKey>(key: T) => Promise<[SymmetricCryptoKey, EncString]>;
/**
* Clears the user's stored organization keys
* @param memoryOnly Clear only the in-memory keys
* @param userId The desired user
*/
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<void>;
/**
* Stores the encrypted provider keys and clears any decrypted
* provider keys currently in memory
* @param providers The providers to set keys for
*/
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
/**
* @param providerId The desired provider
* @returns The provider's symmetric key
*/
getProviderKey: (providerId: string) => Promise<ProviderKey>;
/**
* @returns A map of the provider Ids to their symmetric keys
*/
getProviderKeys: () => Promise<Map<string, ProviderKey>>;
/**
* @param memoryOnly Clear only the in-memory keys
* @param userId The desired user
*/
clearProviderKeys: (memoryOnly?: boolean, userId?: string) => Promise<void>;
/**
* Returns the public key from memory. If not available, extracts it
* from the private key and stores it in memory
* @returns The user's public key
*/
getPublicKey: () => Promise<Uint8Array>;
/**
* Creates a new organization key and encrypts it with the user's public key.
* This method can also return Provider keys for creating new Provider users.
* @returns The new encrypted org key and the decrypted key itself
*/
makeOrgKey: <T extends OrgKey | ProviderKey>() => Promise<[EncString, T]>;
/**
* Sets the 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
*/
setPrivateKey: (encPrivateKey: string) => Promise<void>;
/**
* Returns the private key from memory. If not available, decrypts it
* from storage and stores it in memory
* @returns The user's private key
*/
getPrivateKey: () => Promise<Uint8Array>;
/**
* Generates a fingerprint phrase for the user based on their public key
* @param fingerprintMaterial Fingerprint material
* @param publicKey The user's public key
* @returns The user's fingerprint phrase
*/
getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise<string[]>;
/**
* Generates a new keypair
* @param key A key to encrypt the private key with. If not provided,
* defaults to the user key
* @returns A new keypair: [publicKey in Base64, encrypted privateKey]
*/
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
/**
* Clears the user's key pair
* @param memoryOnly Clear only the in-memory keys
* @param userId The desired user
*/
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<void[]>;
/**
* @param pin The user's pin
* @param salt The user's salt
* @param kdf The user's kdf
* @param kdfConfig The user's kdf config
* @returns A key derived from the user's pin
*/
makePinKey: (pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig) => Promise<PinKey>;
/**
* Clears the user's pin keys from storage
* Note: This will remove the stored pin and as a result,
* disable pin protection for the user
* @param userId The desired user
*/
clearPinKeys: (userId?: string) => Promise<void>;
/**
* Decrypts the user key with their pin
* @param pin The user's PIN
* @param salt The user's salt
* @param kdf The user's KDF
* @param kdfConfig The user's KDF config
* @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided
* it will be retrieved from storage
* @returns The decrypted user key
*/
decryptUserKeyWithPin: (
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig,
protectedKeyCs?: EncString
) => Promise<SymmetricCryptoKey>;
makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>;
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
makePinKey: (
) => Promise<UserKey>;
/**
* Creates a new Pin key that encrypts the user key instead of the
* master key. Clears the old Pin key from state.
* @param masterPasswordOnRestart True if Master Password on Restart is enabled
* @param pin User's PIN
* @param email User's email
* @param kdf User's KdfType
* @param kdfConfig User's KdfConfig
* @param oldPinKey The old Pin key from state (retrieved from different
* places depending on if Master Password on Restart was enabled)
* @returns The user key
*/
decryptAndMigrateOldPinKey: (
masterPasswordOnRestart: boolean,
pin: string,
email: string,
kdf: KdfType,
kdfConfig: KdfConfig,
oldPinKey: EncString
) => Promise<UserKey>;
/**
* Replaces old master auto keys with new user auto keys
*/
migrateAutoKeyIfNeeded: (userId?: string) => Promise<void>;
/**
* @param keyMaterial The key material to derive the send key from
* @returns A new send key
*/
makeSendKey: (keyMaterial: Uint8Array) => Promise<SymmetricCryptoKey>;
/**
* Clears all of the user's keys from storage
* @param userId The user's Id
*/
clearKeys: (userId?: string) => Promise<any>;
/**
* RSA encrypts a value.
* @param data The data to encrypt
* @param publicKey The public key to use for encryption, if not provided, the user's public key will be used
* @returns The encrypted data
*/
rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise<EncString>;
/**
* Decrypts a value using RSA.
* @param encValue The encrypted value to decrypt
* @param privateKeyValue The private key to use for decryption
* @returns The decrypted value
*/
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
randomNumber: (min: number, max: number) => Promise<number>;
/**
* Initialize all necessary crypto keys needed for a new account.
* Warning! This completely replaces any existing keys!
* @returns The user's newly created public key, private key, and encrypted private key
*/
initAccount: () => Promise<{
userKey: UserKey;
publicKey: string;
privateKey: EncString;
}>;
/**
* @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
*/
decryptMasterKeyWithPin: (
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig
) => Promise<SymmetricCryptoKey>;
makeSendKey: (keyMaterial: Uint8Array) => Promise<SymmetricCryptoKey>;
hashPassword: (
password: string,
key: SymmetricCryptoKey,
hashPurpose?: HashPurpose
) => Promise<string>;
makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>;
remakeEncKey: (
key: SymmetricCryptoKey,
encKey?: SymmetricCryptoKey
) => Promise<[SymmetricCryptoKey, EncString]>;
kdfConfig: KdfConfig,
protectedKeyCs?: EncString
) => Promise<MasterKey>;
/**
* Previously, the master key was used for any additional key like the biometrics or pin key.
* We have switched to using the user key for these purposes. This method is for clearing the state
* of the older keys on logout or post migration.
* @param keySuffix The desired type of key to clear
* @param userId The desired user
*/
clearDeprecatedKeys: (keySuffix: KeySuffixOptions, userId?: string) => Promise<void>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.encrypt
*/
encrypt: (plainValue: string | Uint8Array, key?: SymmetricCryptoKey) => Promise<EncString>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.encryptToBytes
*/
encryptToBytes: (plainValue: Uint8Array, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise<EncString>;
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.decryptToBytes
*/
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<Uint8Array>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.decryptToUtf8
*/
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.decryptToBytes
*/
decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise<Uint8Array>;
randomNumber: (min: number, max: number) => Promise<number>;
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
}

View File

@@ -5,6 +5,7 @@ import { OrganizationData } from "../../admin-console/models/data/organization.d
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
@@ -23,10 +24,19 @@ import { CipherView } from "../../vault/models/view/cipher.view";
import { CollectionView } from "../../vault/models/view/collection.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { ServerConfigData } from "../models/data/server-config.data";
import { Account, AccountSettingsSettings } from "../models/domain/account";
import {
Account,
AccountDecryptionOptions,
AccountSettingsSettings,
} from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options";
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import {
DeviceKey,
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../models/domain/symmetric-crypto-key";
export abstract class StateService<T extends Account = Account> {
accounts$: Observable<{ [userId: string]: T }>;
@@ -71,24 +81,106 @@ export abstract class StateService<T extends Account = Account> {
setCollapsedGroupings: (value: string[], options?: StorageOptions) => Promise<void>;
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
/**
* gets the user key
*/
getUserKey: (options?: StorageOptions) => Promise<UserKey>;
/**
* Sets the user key
*/
setUserKey: (value: UserKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's master key
*/
getMasterKey: (options?: StorageOptions) => Promise<MasterKey>;
/**
* Sets the user's master key
*/
setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user key encrypted by the master key
*/
getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user key encrypted by the master key
*/
setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's auto key
*/
getUserKeyAutoUnlock: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user's auto key
*/
setUserKeyAutoUnlock: (value: string, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's biometric key
*/
getUserKeyBiometric: (options?: StorageOptions) => Promise<string>;
/**
* Checks if the user has a biometric key available
*/
hasUserKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
/**
* Sets the user's biometric key
*/
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is disabled
*/
getPinKeyEncryptedUserKey: (options?: StorageOptions) => Promise<EncString>;
/**
* Sets the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is disabled
*/
setPinKeyEncryptedUserKey: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* Gets the ephemeral version of the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is enabled
*/
getPinKeyEncryptedUserKeyEphemeral: (options?: StorageOptions) => Promise<EncString>;
/**
* Sets the ephemeral version of the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is enabled
*/
setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For migration purposes only, use getUserKeyMasterKey instead
*/
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use setUserKeyMasterKey instead
*/
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For legacy purposes only, use getMasterKey instead
*/
getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For migration purposes only, use getUserKeyAuto instead
*/
getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use setUserKeyAuto instead
*/
setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise<void>;
getCryptoMasterKeyB64: (options?: StorageOptions) => Promise<string>;
setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For migration purposes only, use getUserKeyBiometric instead
*/
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use hasUserKeyBiometric instead
*/
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
/**
* @deprecated For migration purposes only, use setUserKeyBiometric instead
*/
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
getDecryptedCollections: (options?: StorageOptions) => Promise<CollectionView[]>;
setDecryptedCollections: (value: CollectionView[], options?: StorageOptions) => Promise<void>;
getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
setDecryptedCryptoSymmetricKey: (
value: SymmetricCryptoKey,
options?: StorageOptions
) => Promise<void>;
getDecryptedOrganizationKeys: (
options?: StorageOptions
) => Promise<Map<string, SymmetricCryptoKey>>;
@@ -103,7 +195,13 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[],
options?: StorageOptions
) => Promise<void>;
/**
* @deprecated For migration purposes only, use getDecryptedUserKeyPin instead
*/
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
/**
* @deprecated For migration purposes only, use setDecryptedUserKeyPin instead
*/
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this, use PolicyService
@@ -164,7 +262,21 @@ export abstract class StateService<T extends Account = Account> {
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise<void>;
setDeviceKey: (value: DeviceKey | null, options?: StorageOptions) => Promise<void>;
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
setAdminAuthRequest: (
adminAuthRequest: AdminAuthRequestStorable,
options?: StorageOptions
) => Promise<void>;
getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>;
setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>;
getAccountDecryptionOptions: (
options?: StorageOptions
) => Promise<AccountDecryptionOptions | null>;
setAccountDecryptionOptions: (
value: AccountDecryptionOptions,
options?: StorageOptions
) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>;
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
@@ -205,8 +317,6 @@ export abstract class StateService<T extends Account = Account> {
value: { [id: string]: CollectionData },
options?: StorageOptions
) => Promise<void>;
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use FolderService
*/
@@ -232,7 +342,13 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[],
options?: StorageOptions
) => Promise<void>;
/**
* @deprecated For migration purposes only, use getEncryptedUserKeyPin instead
*/
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use setEncryptedUserKeyPin instead
*/
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use PolicyService
@@ -269,6 +385,8 @@ export abstract class StateService<T extends Account = Account> {
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
getEverHadUserKey: (options?: StorageOptions) => Promise<boolean>;
setEverHadUserKey: (value: boolean, options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
getForcePasswordResetReason: (options?: StorageOptions) => Promise<ForceResetPasswordReason>;
@@ -327,7 +445,13 @@ export abstract class StateService<T extends Account = Account> {
setUsernameGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
getGeneratorOptions: (options?: StorageOptions) => Promise<any>;
setGeneratorOptions: (value: any, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's Pin, encrypted by the user key
*/
getProtectedPin: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user's Pin, encrypted by the user key
*/
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
@@ -353,6 +477,11 @@ export abstract class StateService<T extends Account = Account> {
setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise<void>;
getSsoState: (options?: StorageOptions) => Promise<string>;
setSsoState: (value: string, options?: StorageOptions) => Promise<void>;
getUserSsoOrganizationIdentifier: (options?: StorageOptions) => Promise<string>;
setUserSsoOrganizationIdentifier: (
value: string | null,
options?: StorageOptions
) => Promise<void>;
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;

View File

@@ -1,8 +1,9 @@
import { makeStaticByteArray } from "../../../../spec";
import { CsprngArray } from "../../../types/csprng";
import { Utils } from "../../misc/utils";
import { AccountKeys, EncryptionPair } from "./account";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
describe("AccountKeys", () => {
describe("toJSON", () => {
@@ -22,6 +23,23 @@ describe("AccountKeys", () => {
const json = JSON.stringify(keys);
expect(json).toContain('"publicKey":"hello"');
});
// As the accountKeys.toJSON doesn't really serialize the device key
// this method just checks the persistence of the deviceKey
it("should persist deviceKey", () => {
// Arrange
const accountKeys = new AccountKeys();
const deviceKeyBytesLength = 64;
accountKeys.deviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
) as DeviceKey;
// Act
const serializedKeys = accountKeys.toJSON();
// Assert
expect(serializedKeys.deviceKey).toEqual(accountKeys.deviceKey);
});
});
describe("fromJSON", () => {
@@ -57,5 +75,24 @@ describe("AccountKeys", () => {
} as any);
expect(spy).toHaveBeenCalled();
});
it("should deserialize deviceKey", () => {
// Arrange
const expectedKeyB64 =
"ZJNnhx9BbJeb2EAq1hlMjqt6GFsg9G/GzoFf6SbPKsaiMhKGDcbHcwcyEg56Lh8lfilpZz4SRM6UA7oFCg+lSg==";
const symmetricCryptoKeyFromJsonSpy = jest.spyOn(SymmetricCryptoKey, "fromJSON");
// Act
const accountKeys = AccountKeys.fromJSON({
deviceKey: {
keyB64: expectedKeyB64,
},
} as any);
// Assert
expect(symmetricCryptoKeyFromJsonSpy).toHaveBeenCalled();
expect(accountKeys.deviceKey.keyB64).toEqual(expectedKeyB64);
});
});
});

View File

@@ -6,8 +6,12 @@ import { PolicyData } from "../../../admin-console/models/data/policy.data";
import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { Policy } from "../../../admin-console/models/domain/policy";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { UserDecryptionOptionsResponse } from "../../../auth/models/response/user-decryption-options/user-decryption-options.response";
import { KdfType, UriMatchType } from "../../../enums";
import { EventData } from "../../../models/data/event.data";
import { GeneratedPasswordHistory } from "../../../tools/generator/password";
@@ -22,8 +26,8 @@ import { CollectionView } from "../../../vault/models/view/collection.view";
import { Utils } from "../../misc/utils";
import { ServerConfigData } from "../../models/data/server-config.data";
import { EncString } from "./enc-string";
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
import { EncryptedString, EncString } from "./enc-string";
import { MasterKey, SymmetricCryptoKey, UserKey } from "./symmetric-crypto-key";
export class EncryptionPair<TEncrypted, TDecrypted> {
encrypted?: TEncrypted;
@@ -99,15 +103,10 @@ export class AccountData {
}
export class AccountKeys {
cryptoMasterKey?: SymmetricCryptoKey;
cryptoMasterKeyAuto?: string;
cryptoMasterKeyB64?: string;
cryptoMasterKeyBiometric?: string;
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
string,
SymmetricCryptoKey
>();
deviceKey?: DeviceKey;
userKey?: UserKey;
masterKey?: MasterKey;
masterKeyEncryptedUserKey?: string;
deviceKey?: ReturnType<SymmetricCryptoKey["toJSON"]>;
organizationKeys?: EncryptionPair<
{ [orgId: string]: EncryptedOrganizationKeyData },
Record<string, SymmetricCryptoKey>
@@ -123,6 +122,18 @@ export class AccountKeys {
publicKey?: Uint8Array;
apiKeyClientSecret?: string;
/** @deprecated July 2023, left for migration purposes*/
cryptoMasterKey?: SymmetricCryptoKey;
/** @deprecated July 2023, left for migration purposes*/
cryptoMasterKeyAuto?: string;
/** @deprecated July 2023, left for migration purposes*/
cryptoMasterKeyBiometric?: string;
/** @deprecated July 2023, left for migration purposes*/
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
string,
SymmetricCryptoKey
>();
toJSON() {
return Utils.merge(this, {
publicKey: Utils.fromBufferToByteString(this.publicKey),
@@ -135,6 +146,9 @@ export class AccountKeys {
}
return Object.assign(new AccountKeys(), {
userKey: SymmetricCryptoKey.fromJSON(obj?.userKey),
masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey),
deviceKey: obj?.deviceKey,
cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey),
cryptoSymmetricKey: EncryptionPair.fromJSON(
obj?.cryptoSymmetricKey,
@@ -173,6 +187,7 @@ export class AccountProfile {
emailVerified?: boolean;
entityId?: string;
entityType?: string;
everHadUserKey?: boolean;
everBeenUnlocked?: boolean;
forcePasswordResetReason?: ForceResetPasswordReason;
hasPremiumPersonally?: boolean;
@@ -223,7 +238,8 @@ export class AccountSettings {
passwordGenerationOptions?: any;
usernameGenerationOptions?: any;
generatorOptions?: any;
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
pinKeyEncryptedUserKey?: EncryptedString;
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
protectedPin?: string;
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number;
@@ -234,6 +250,10 @@ export class AccountSettings {
activateAutoFillOnPageLoadFromPolicy?: boolean;
region?: string;
smOnboardingTasks?: Record<string, Record<string, boolean>>;
trustDeviceChoiceForDecryption?: boolean;
/** @deprecated July 2023, left for migration purposes*/
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
if (obj == null) {
@@ -269,12 +289,106 @@ export class AccountTokens {
}
}
export class AccountDecryptionOptions {
hasMasterPassword: boolean;
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
keyConnectorOption?: KeyConnectorUserDecryptionOption;
constructor(init?: Partial<AccountDecryptionOptions>) {
if (init) {
Object.assign(this, init);
}
}
// TODO: these nice getters don't work because the Account object is not properly being deserialized out of
// JSON (the Account static fromJSON method is not running) so these getters don't exist on the
// account decryptions options object when pulled out of state. This is a bug that needs to be fixed later on
// get hasTrustedDeviceOption(): boolean {
// return this.trustedDeviceOption !== null && this.trustedDeviceOption !== undefined;
// }
// get hasKeyConnectorOption(): boolean {
// return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined;
// }
static fromResponse(response: UserDecryptionOptionsResponse): AccountDecryptionOptions {
if (response == null) {
return null;
}
const accountDecryptionOptions = new AccountDecryptionOptions();
accountDecryptionOptions.hasMasterPassword = response.hasMasterPassword;
if (response.trustedDeviceOption) {
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
response.trustedDeviceOption.hasAdminApproval,
response.trustedDeviceOption.hasLoginApprovingDevice,
response.trustedDeviceOption.hasManageResetPasswordPermission
);
}
if (response.keyConnectorOption) {
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
response.keyConnectorOption.keyConnectorUrl
);
}
return accountDecryptionOptions;
}
static fromJSON(obj: Jsonify<AccountDecryptionOptions>): AccountDecryptionOptions {
if (obj == null) {
return null;
}
const accountDecryptionOptions = Object.assign(new AccountDecryptionOptions(), obj);
if (obj.trustedDeviceOption) {
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
obj.trustedDeviceOption.hasAdminApproval,
obj.trustedDeviceOption.hasLoginApprovingDevice,
obj.trustedDeviceOption.hasManageResetPasswordPermission
);
}
if (obj.keyConnectorOption) {
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
obj.keyConnectorOption.keyConnectorUrl
);
}
return accountDecryptionOptions;
}
}
export class LoginState {
ssoOrganizationIdentifier?: string;
constructor(init?: Partial<LoginState>) {
if (init) {
Object.assign(this, init);
}
}
static fromJSON(obj: Jsonify<LoginState>): LoginState {
if (obj == null) {
return null;
}
const loginState = Object.assign(new LoginState(), obj);
return loginState;
}
}
export class Account {
data?: AccountData = new AccountData();
keys?: AccountKeys = new AccountKeys();
profile?: AccountProfile = new AccountProfile();
settings?: AccountSettings = new AccountSettings();
tokens?: AccountTokens = new AccountTokens();
decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions();
loginState?: LoginState = new LoginState();
adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null;
constructor(init: Partial<Account>) {
Object.assign(this, {
@@ -298,6 +412,15 @@ export class Account {
...new AccountTokens(),
...init?.tokens,
},
decryptionOptions: {
...new AccountDecryptionOptions(),
...init?.decryptionOptions,
},
loginState: {
...new LoginState(),
...init?.loginState,
},
adminAuthRequest: init?.adminAuthRequest,
});
}
@@ -311,6 +434,9 @@ export class Account {
profile: AccountProfile.fromJSON(json?.profile),
settings: AccountSettings.fromJSON(json?.settings),
tokens: AccountTokens.fromJSON(json?.tokens),
decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions),
loginState: LoginState.fromJSON(json?.loginState),
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
});
}
}

View File

@@ -4,7 +4,11 @@ import { mock, MockProxy } from "jest-mock-extended";
import { EncryptionType } from "../../../enums";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import {
OrgKey,
SymmetricCryptoKey,
UserKey,
} from "../../../platform/models/domain/symmetric-crypto-key";
import { CryptoService } from "../../abstractions/crypto.service";
import { ContainerService } from "../../services/container.service";
@@ -225,12 +229,12 @@ describe("EncString", () => {
await encString.decrypt(null, key);
expect(cryptoService.getKeyForUserEncryption).not.toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
});
it("gets an organization key if required", async () => {
const orgKey = mock<SymmetricCryptoKey>();
const orgKey = mock<OrgKey>();
cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
@@ -241,13 +245,13 @@ describe("EncString", () => {
});
it("gets the user's decryption key if required", async () => {
const userKey = mock<SymmetricCryptoKey>();
const userKey = mock<UserKey>();
cryptoService.getKeyForUserEncryption.mockResolvedValue(userKey);
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
await encString.decrypt(null, null);
expect(cryptoService.getKeyForUserEncryption).toHaveBeenCalledWith();
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, userKey);
});
});

View File

@@ -1,4 +1,4 @@
import { Jsonify } from "type-fest";
import { Jsonify, Opaque } from "type-fest";
import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../enums";
import { Utils } from "../../../platform/misc/utils";
@@ -7,7 +7,7 @@ import { Encrypted } from "../../interfaces/encrypted";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
export class EncString implements Encrypted {
encryptedString?: string;
encryptedString?: EncryptedString;
encryptionType?: EncryptionType;
decryptedValue?: string;
data?: string;
@@ -53,14 +53,14 @@ export class EncString implements Encrypted {
private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) {
if (iv != null) {
this.encryptedString = encType + "." + iv + "|" + data;
this.encryptedString = (encType + "." + iv + "|" + data) as EncryptedString;
} else {
this.encryptedString = encType + "." + data;
this.encryptedString = (encType + "." + data) as EncryptedString;
}
// mac
if (mac != null) {
this.encryptedString += "|" + mac;
this.encryptedString = (this.encryptedString + "|" + mac) as EncryptedString;
}
this.encryptionType = encType;
@@ -70,7 +70,7 @@ export class EncString implements Encrypted {
}
private initFromEncryptedString(encryptedString: string) {
this.encryptedString = encryptedString as string;
this.encryptedString = encryptedString as EncryptedString;
if (!this.encryptedString) {
return;
}
@@ -162,6 +162,8 @@ export class EncString implements Encrypted {
const cryptoService = Utils.getContainerService().getCryptoService();
return orgId != null
? await cryptoService.getOrgKey(orgId)
: await cryptoService.getKeyForUserEncryption();
: await cryptoService.getUserKeyWithLegacySupport();
}
}
export type EncryptedString = Opaque<string, "EncString">;

View File

@@ -78,3 +78,8 @@ export class SymmetricCryptoKey {
// Setup all separate key types as opaque types
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
export type UserKey = Opaque<SymmetricCryptoKey, "UserKey">;
export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">;
export type ProviderKey = Opaque<SymmetricCryptoKey, "ProviderKey">;

View File

@@ -36,16 +36,25 @@ export class ConfigService implements ConfigServiceAbstraction {
try {
const response = await this.configApiService.get();
if (response != null) {
const data = new ServerConfigData(response);
const serverConfig = new ServerConfig(data);
this._serverConfig.next(serverConfig);
if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) {
return serverConfig;
}
if (response == null) {
return;
}
const data = new ServerConfigData(response);
const serverConfig = new ServerConfig(data);
this._serverConfig.next(serverConfig);
const userAuthStatus = await this.authService.getAuthStatus();
if (userAuthStatus !== AuthenticationStatus.LoggedOut) {
// Store the config for offline use if the user is logged in
await this.stateService.setServerConfig(data);
this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion);
}
// Always return new server config from server to calling method
// to ensure up to date information
// This change is specifically for the getFeatureFlag > buildServerConfig flow
// for locked or logged in users.
return serverConfig;
} catch {
return null;
}

View File

@@ -1,10 +1,18 @@
import { mock, mockReset } from "jest-mock-extended";
import { CsprngArray } from "../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import { EncString } from "../models/domain/enc-string";
import {
MasterKey,
PinKey,
SymmetricCryptoKey,
UserKey,
} from "../models/domain/symmetric-crypto-key";
import { CryptoService } from "../services/crypto.service";
describe("cryptoService", () => {
@@ -16,6 +24,8 @@ describe("cryptoService", () => {
const logService = mock<LogService>();
const stateService = mock<StateService>();
const mockUserId = "mock user id";
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
@@ -35,4 +45,172 @@ describe("cryptoService", () => {
it("instantiates", () => {
expect(cryptoService).not.toBeFalsy();
});
describe("getUserKey", () => {
let mockUserKey: UserKey;
let stateSvcGetUserKey: jest.SpyInstance;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
stateSvcGetUserKey = jest.spyOn(stateService, "getUserKey");
});
it("returns the User Key if available", async () => {
stateSvcGetUserKey.mockResolvedValue(mockUserKey);
const userKey = await cryptoService.getUserKey(mockUserId);
expect(stateSvcGetUserKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(userKey).toEqual(mockUserKey);
});
it("sets the Auto key if the User Key if not set", async () => {
const autoKeyB64 =
"IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g==";
stateService.getUserKeyAutoUnlock.mockResolvedValue(autoKeyB64);
const userKey = await cryptoService.getUserKey(mockUserId);
expect(stateService.setUserKey).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), {
userId: mockUserId,
});
expect(userKey.keyB64).toEqual(autoKeyB64);
});
});
describe("getUserKeyWithLegacySupport", () => {
let mockUserKey: UserKey;
let mockMasterKey: MasterKey;
let stateSvcGetUserKey: jest.SpyInstance;
let stateSvcGetMasterKey: jest.SpyInstance;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
stateSvcGetUserKey = jest.spyOn(stateService, "getUserKey");
stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey");
});
it("returns the User Key if available", async () => {
stateSvcGetUserKey.mockResolvedValue(mockUserKey);
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(stateSvcGetUserKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(stateSvcGetMasterKey).not.toHaveBeenCalled();
expect(userKey).toEqual(mockUserKey);
});
it("returns the user's master key when User Key is not available", async () => {
stateSvcGetUserKey.mockResolvedValue(null);
stateSvcGetMasterKey.mockResolvedValue(mockMasterKey);
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(stateSvcGetMasterKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(userKey).toEqual(mockMasterKey);
});
});
describe("setUserKey", () => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
describe("Auto Key refresh", () => {
it("sets an Auto key if vault timeout is set to null", async () => {
stateService.getVaultTimeout.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(mockUserKey.keyB64, {
userId: mockUserId,
});
});
it("clears the Auto key if vault timeout is set to anything other than null", async () => {
stateService.getVaultTimeout.mockResolvedValue(10);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
it("clears the old deprecated Auto key whenever a User Key is set", async () => {
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setCryptoMasterKeyAuto).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
});
describe("Pin Key refresh", () => {
let cryptoSvcMakePinKey: jest.SpyInstance;
const protectedPin =
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=";
let encPin: EncString;
beforeEach(() => {
cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey");
cryptoSvcMakePinKey.mockResolvedValue(new SymmetricCryptoKey(new Uint8Array(64)) as PinKey);
encPin = new EncString(
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg="
);
encryptService.encrypt.mockResolvedValue(encPin);
});
it("sets a UserKeyPin if a ProtectedPin and UserKeyPin is set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(
new EncString(
"2.OdGNE3L23GaDZGvu9h2Brw==|/OAcNnrYwu0rjiv8+RUr3Tc+Ef8fV035Tm1rbTxfEuC+2LZtiCAoIvHIZCrM/V1PWnb/pHO2gh9+Koks04YhX8K29ED4FzjeYP8+YQD/dWo=|+12xTcIK/UVRsOyawYudPMHb6+lCHeR2Peq1pQhPm0A="
)
);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(expect.any(EncString), {
userId: mockUserId,
});
});
it("sets a PinKeyEphemeral if a ProtectedPin is set, but a UserKeyPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(
expect.any(EncString),
{
userId: mockUserId,
}
);
});
it("clears the UserKeyPin and UserKeyPinEphemeral if the ProtectedPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import { OrganizationData } from "../../admin-console/models/data/organization.d
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
@@ -43,6 +44,7 @@ import { ServerConfigData } from "../models/data/server-config.data";
import {
Account,
AccountData,
AccountDecryptionOptions,
AccountSettings,
AccountSettingsSettings,
} from "../models/domain/account";
@@ -50,7 +52,12 @@ import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state";
import { State } from "../models/domain/state";
import { StorageOptions } from "../models/domain/storage-options";
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import {
DeviceKey,
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../models/domain/symmetric-crypto-key";
const keys = {
state: "state",
@@ -62,6 +69,9 @@ const keys = {
};
const partialKeys = {
userAutoKey: "_user_auto",
userBiometricKey: "_user_biometric",
autoKey: "_masterkey_auto",
biometricKey: "_masterkey_biometric",
masterKey: "_masterkey",
@@ -112,7 +122,7 @@ export class StateService<
// FIXME: This should be refactored into AuthService or a similar service,
// as checking for the existence of the crypto key is a low level
// implementation detail.
this.activeAccountUnlockedSubject.next((await this.getCryptoMasterKey()) != null);
this.activeAccountUnlockedSubject.next((await this.getUserKey()) != null);
})
)
.subscribe();
@@ -515,6 +525,9 @@ export class StateService<
);
}
/**
* @deprecated Do not save the Master Key. Use the User Symmetric Key instead
*/
async getCryptoMasterKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -522,6 +535,9 @@ export class StateService<
return account?.keys?.cryptoMasterKey;
}
/**
* @deprecated Do not save the Master Key. Use the User Symmetric Key instead
*/
async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -542,6 +558,203 @@ export class StateService<
}
}
/**
* user key used to encrypt/decrypt data
*/
async getUserKey(options?: StorageOptions): Promise<UserKey> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return account?.keys?.userKey as UserKey;
}
/**
* user key used to encrypt/decrypt data
*/
async setUserKey(value: UserKey, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.userKey = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
if (options?.userId == this.activeAccountSubject.getValue()) {
const nextValue = value != null;
// Avoid emitting if we are already unlocked
if (this.activeAccountUnlockedSubject.getValue() != nextValue) {
this.activeAccountUnlockedSubject.next(nextValue);
}
}
}
/**
* User's master key derived from MP, saved only if we decrypted with MP
*/
async getMasterKey(options?: StorageOptions): Promise<MasterKey> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return account?.keys?.masterKey;
}
/**
* User's master key derived from MP, saved only if we decrypted with MP
*/
async setMasterKey(value: MasterKey, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.masterKey = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
/**
* The master key encrypted User symmetric key, saved on every auth
* so we can unlock with MP offline
*/
async getMasterKeyEncryptedUserKey(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.keys.masterKeyEncryptedUserKey;
}
/**
* The master key encrypted User symmetric key, saved on every auth
* so we can unlock with MP offline
*/
async setMasterKeyEncryptedUserKey(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.keys.masterKeyEncryptedUserKey = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
/**
* user key when using the "never" option of vault timeout
*/
async getUserKeyAutoUnlock(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}${partialKeys.userAutoKey}`,
options
);
}
/**
* user key when using the "never" option of vault timeout
*/
async setUserKeyAutoUnlock(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return;
}
await this.saveSecureStorageKey(partialKeys.userAutoKey, value, options);
}
/**
* User's encrypted symmetric key when using biometrics
*/
async getUserKeyBiometric(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}${partialKeys.userBiometricKey}`,
options
);
}
async hasUserKeyBiometric(options?: StorageOptions): Promise<boolean> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return false;
}
return await this.secureStorageService.has(
`${options.userId}${partialKeys.userBiometricKey}`,
options
);
}
async setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return;
}
await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options);
}
async getPinKeyEncryptedUserKey(options?: StorageOptions): Promise<EncString> {
return EncString.fromJSON(
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.settings?.pinKeyEncryptedUserKey
);
}
async setPinKeyEncryptedUserKey(value: EncString, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.settings.pinKeyEncryptedUserKey = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getPinKeyEncryptedUserKeyEphemeral(options?: StorageOptions): Promise<EncString> {
return EncString.fromJSON(
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
?.settings?.pinKeyEncryptedUserKeyEphemeral
);
}
async setPinKeyEncryptedUserKeyEphemeral(
value: EncString,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.settings.pinKeyEncryptedUserKeyEphemeral = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
/**
* @deprecated Use UserKeyAuto instead
*/
async getCryptoMasterKeyAuto(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }),
@@ -556,6 +769,9 @@ export class StateService<
);
}
/**
* @deprecated Use UserKeyAuto instead
*/
async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }),
@@ -567,6 +783,9 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.autoKey, value, options);
}
/**
* @deprecated I don't see where this is even used
*/
async getCryptoMasterKeyB64(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
@@ -578,6 +797,9 @@ export class StateService<
);
}
/**
* @deprecated I don't see where this is even used
*/
async setCryptoMasterKeyB64(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
@@ -586,6 +808,9 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.masterKey, value, options);
}
/**
* @deprecated Use UserKeyBiometric instead
*/
async getCryptoMasterKeyBiometric(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
@@ -600,6 +825,9 @@ export class StateService<
);
}
/**
* @deprecated Use UserKeyBiometric instead
*/
async hasCryptoMasterKeyBiometric(options?: StorageOptions): Promise<boolean> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
@@ -614,6 +842,9 @@ export class StateService<
);
}
/**
* @deprecated Use UserKeyBiometric instead
*/
async setCryptoMasterKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
@@ -661,6 +892,9 @@ export class StateService<
);
}
/**
* @deprecated Use UserKey instead
*/
async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -668,6 +902,9 @@ export class StateService<
return account?.keys?.cryptoSymmetricKey?.decrypted;
}
/**
* @deprecated Use UserKey instead
*/
async setDecryptedCryptoSymmetricKey(
value: SymmetricCryptoKey,
options?: StorageOptions
@@ -728,12 +965,18 @@ export class StateService<
);
}
/**
* @deprecated Use getPinKeyEncryptedUserKeyEphemeral instead
*/
async getDecryptedPinProtected(options?: StorageOptions): Promise<EncString> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.settings?.pinProtected?.decrypted;
}
/**
* @deprecated Use setPinKeyEncryptedUserKeyEphemeral instead
*/
async setDecryptedPinProtected(value: EncString, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -1069,10 +1312,17 @@ export class StateService<
const account = await this.getAccount(options);
return account?.keys?.deviceKey as DeviceKey;
const existingDeviceKey = account?.keys?.deviceKey;
// Must manually instantiate the SymmetricCryptoKey class from the JSON object
if (existingDeviceKey != null) {
return SymmetricCryptoKey.fromJSON(existingDeviceKey) as DeviceKey;
} else {
return null;
}
}
async setDeviceKey(value: DeviceKey, options?: StorageOptions): Promise<void> {
async setDeviceKey(value: DeviceKey | null, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
@@ -1081,7 +1331,94 @@ export class StateService<
const account = await this.getAccount(options);
account.keys.deviceKey = value;
account.keys.deviceKey = value?.toJSON() ?? null;
await this.saveAccount(account, options);
}
async getAdminAuthRequest(options?: StorageOptions): Promise<AdminAuthRequestStorable | null> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return null;
}
const account = await this.getAccount(options);
return account?.adminAuthRequest
? AdminAuthRequestStorable.fromJSON(account.adminAuthRequest)
: null;
}
async setAdminAuthRequest(
adminAuthRequest: AdminAuthRequestStorable,
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return;
}
const account = await this.getAccount(options);
account.adminAuthRequest = adminAuthRequest?.toJSON();
await this.saveAccount(account, options);
}
async getShouldTrustDevice(options?: StorageOptions): Promise<boolean | null> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return null;
}
const account = await this.getAccount(options);
return account?.settings?.trustDeviceChoiceForDecryption ?? null;
}
async setShouldTrustDevice(value: boolean, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return;
}
const account = await this.getAccount(options);
account.settings.trustDeviceChoiceForDecryption = value;
await this.saveAccount(account, options);
}
async getAccountDecryptionOptions(
options?: StorageOptions
): Promise<AccountDecryptionOptions | null> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return null;
}
const account = await this.getAccount(options);
return account?.decryptionOptions as AccountDecryptionOptions;
}
async setAccountDecryptionOptions(
value: AccountDecryptionOptions,
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return;
}
const account = await this.getAccount(options);
account.decryptionOptions = value;
await this.saveAccount(account, options);
}
@@ -1366,12 +1703,18 @@ export class StateService<
);
}
/**
* @deprecated Use UserKey instead
*/
async getEncryptedCryptoSymmetricKey(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.keys.cryptoSymmetricKey.encrypted;
}
/**
* @deprecated Use UserKey instead
*/
async setEncryptedCryptoSymmetricKey(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
@@ -1655,6 +1998,24 @@ export class StateService<
);
}
async getEverHadUserKey(options?: StorageOptions): Promise<boolean> {
return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.profile?.everHadUserKey ?? false
);
}
async setEverHadUserKey(value: boolean, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.profile.everHadUserKey = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
@@ -2229,6 +2590,26 @@ export class StateService<
);
}
async getUserSsoOrganizationIdentifier(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.loginState?.ssoOrganizationIdentifier;
}
async setUserSsoOrganizationIdentifier(
value: string | null,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.loginState.ssoOrganizationIdentifier = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getTheme(options?: StorageOptions): Promise<ThemeType> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
@@ -2783,6 +3164,8 @@ export class StateService<
protected async removeAccountFromSecureStorage(userId: string = null): Promise<void> {
userId = userId ?? (await this.state())?.activeUserId;
await this.setUserKeyAutoUnlock(null, { userId: userId });
await this.setUserKeyBiometric(null, { userId: userId });
await this.setCryptoMasterKeyAuto(null, { userId: userId });
await this.setCryptoMasterKeyBiometric(null, { userId: userId });
await this.setCryptoMasterKeyB64(null, { userId: userId });
@@ -2808,11 +3191,12 @@ export class StateService<
}
}
// settings persist even on reset, and are not effected by this method
// settings persist even on reset, and are not affected by this method
protected resetAccount(account: TAccount) {
const persistentAccountInformation = {
settings: account.settings,
keys: { deviceKey: account.keys.deviceKey },
adminAuthRequest: account.adminAuthRequest,
};
return Object.assign(this.createAccount(), persistentAccountInformation);
}
@@ -2940,7 +3324,7 @@ export class StateService<
}
}
private deleteDiskCache(key: string) {
protected deleteDiskCache(key: string) {
if (this.useAccountCache) {
delete this.accountDiskCache.value[key];
this.accountDiskCache.next(this.accountDiskCache.value);

View File

@@ -39,8 +39,8 @@ export class SystemService implements SystemServiceAbstraction {
}
// User has set a PIN, with ask for master password on restart, to protect their vault
const decryptedPinProtected = await this.stateService.getDecryptedPinProtected();
if (decryptedPinProtected != null) {
const ephemeralPin = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
if (ephemeralPin != null) {
return;
}

View File

@@ -251,10 +251,15 @@ export class ApiService implements ApiServiceAbstraction {
}
}
// TODO: PM-3519: Create and move to AuthRequest Api service
async postAuthRequest(request: PasswordlessCreateAuthRequest): Promise<AuthRequestResponse> {
const r = await this.send("POST", "/auth-requests/", request, false, true);
return new AuthRequestResponse(r);
}
async postAdminAuthRequest(request: PasswordlessCreateAuthRequest): Promise<AuthRequestResponse> {
const r = await this.send("POST", "/auth-requests/admin-request", request, true, true);
return new AuthRequestResponse(r);
}
async getAuthResponse(id: string, accessCode: string): Promise<AuthRequestResponse> {
const path = `/auth-requests/${id}/response?code=${accessCode}`;
@@ -1580,7 +1585,9 @@ export class ApiService implements ApiServiceAbstraction {
// Key Connector
async getUserKeyFromKeyConnector(keyConnectorUrl: string): Promise<KeyConnectorUserKeyResponse> {
async getMasterKeyFromKeyConnector(
keyConnectorUrl: string
): Promise<KeyConnectorUserKeyResponse> {
const authHeader = await this.getActiveBearerToken();
const response = await this.fetch(

View File

@@ -1,85 +0,0 @@
import { DeviceCryptoServiceAbstraction } from "../abstractions/device-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
import { AppIdService } from "../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../platform/abstractions/crypto-function.service";
import { CryptoService } from "../platform/abstractions/crypto.service";
import { EncryptService } from "../platform/abstractions/encrypt.service";
import { StateService } from "../platform/abstractions/state.service";
import { SymmetricCryptoKey, DeviceKey } from "../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../types/csprng";
export class DeviceCryptoService implements DeviceCryptoServiceAbstraction {
constructor(
protected cryptoFunctionService: CryptoFunctionService,
protected cryptoService: CryptoService,
protected encryptService: EncryptService,
protected stateService: StateService,
protected appIdService: AppIdService,
protected devicesApiService: DevicesApiServiceAbstraction
) {}
async trustDevice(): Promise<DeviceResponse> {
// Attempt to get user symmetric key
const userSymKey: SymmetricCryptoKey = await this.cryptoService.getEncKey();
// If user symmetric key is not found, throw error
if (!userSymKey) {
throw new Error("User symmetric key not found");
}
// Generate deviceKey
const deviceKey = await this.makeDeviceKey();
// Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey
const [devicePublicKey, devicePrivateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(
2048
);
const [
devicePublicKeyEncryptedUserSymKey,
userSymKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey,
] = await Promise.all([
// Encrypt user symmetric key with the DevicePublicKey
this.cryptoService.rsaEncrypt(userSymKey.encKey, devicePublicKey),
// Encrypt devicePublicKey with user symmetric key
this.encryptService.encrypt(devicePublicKey, userSymKey),
// Encrypt devicePrivateKey with deviceKey
this.encryptService.encrypt(devicePrivateKey, deviceKey),
]);
// Send encrypted keys to server
const deviceIdentifier = await this.appIdService.getAppId();
return this.devicesApiService.updateTrustedDeviceKeys(
deviceIdentifier,
devicePublicKeyEncryptedUserSymKey.encryptedString,
userSymKeyEncryptedDevicePublicKey.encryptedString,
deviceKeyEncryptedDevicePrivateKey.encryptedString
);
}
async getDeviceKey(): Promise<DeviceKey> {
// Check if device key is already stored
const existingDeviceKey = await this.stateService.getDeviceKey();
if (existingDeviceKey != null) {
return existingDeviceKey;
} else {
return this.makeDeviceKey();
}
}
private async makeDeviceKey(): Promise<DeviceKey> {
// Create 512-bit device key
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
// Save device key in secure storage
await this.stateService.setDeviceKey(deviceKey);
return deviceKey;
}
}

View File

@@ -1,317 +0,0 @@
import { mock, mockReset } from "jest-mock-extended";
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
import { EncryptionType } from "../enums/encryption-type.enum";
import { AppIdService } from "../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../platform/abstractions/crypto-function.service";
import { EncryptService } from "../platform/abstractions/encrypt.service";
import { StateService } from "../platform/abstractions/state.service";
import { EncString } from "../platform/models/domain/enc-string";
import { SymmetricCryptoKey, DeviceKey } from "../platform/models/domain/symmetric-crypto-key";
import { CryptoService } from "../platform/services/crypto.service";
import { CsprngArray } from "../types/csprng";
import { DeviceCryptoService } from "./device-crypto.service.implementation";
describe("deviceCryptoService", () => {
let deviceCryptoService: DeviceCryptoService;
const cryptoFunctionService = mock<CryptoFunctionService>();
const cryptoService = mock<CryptoService>();
const encryptService = mock<EncryptService>();
const stateService = mock<StateService>();
const appIdService = mock<AppIdService>();
const devicesApiService = mock<DevicesApiServiceAbstraction>();
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
mockReset(stateService);
mockReset(appIdService);
mockReset(devicesApiService);
deviceCryptoService = new DeviceCryptoService(
cryptoFunctionService,
cryptoService,
encryptService,
stateService,
appIdService,
devicesApiService
);
});
it("instantiates", () => {
expect(deviceCryptoService).not.toBeFalsy();
});
describe("Trusted Device Encryption", () => {
const deviceKeyBytesLength = 64;
const userSymKeyBytesLength = 64;
describe("getDeviceKey", () => {
let mockRandomBytes: CsprngArray;
let mockDeviceKey: SymmetricCryptoKey;
let existingDeviceKey: DeviceKey;
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
let makeDeviceKeySpy: jest.SpyInstance;
beforeEach(() => {
mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes);
existingDeviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength) as CsprngArray
) as DeviceKey;
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
makeDeviceKeySpy = jest.spyOn(deviceCryptoService as any, "makeDeviceKey");
});
it("gets a device key when there is not an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
makeDeviceKeySpy.mockResolvedValue(mockDeviceKey);
const deviceKey = await deviceCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(deviceKey).toEqual(mockDeviceKey);
});
it("returns the existing device key without creating a new one when there is an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
const deviceKey = await deviceCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(makeDeviceKeySpy).not.toHaveBeenCalled();
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(deviceKey).toEqual(existingDeviceKey);
});
});
describe("makeDeviceKey", () => {
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
const cryptoFuncSvcRandomBytesSpy = jest
.spyOn(cryptoFunctionService, "randomBytes")
.mockResolvedValue(mockRandomBytes);
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
// TypeScript will allow calling private methods if the object is of type 'any'
// This is a hacky workaround, but it allows for cleaner tests
const deviceKey = await (deviceCryptoService as any).makeDeviceKey();
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
});
});
describe("trustDevice", () => {
let mockDeviceKeyRandomBytes: CsprngArray;
let mockDeviceKey: DeviceKey;
let mockUserSymKeyRandomBytes: CsprngArray;
let mockUserSymKey: SymmetricCryptoKey;
const deviceRsaKeyLength = 2048;
let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array];
let mockDevicePrivateKey: Uint8Array;
let mockDevicePublicKey: Uint8Array;
let mockDevicePublicKeyEncryptedUserSymKey: EncString;
let mockUserSymKeyEncryptedDevicePublicKey: EncString;
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
Id: "mockId",
Name: "mockName",
Identifier: "mockIdentifier",
Type: "mockType",
CreationDate: "mockCreationDate",
});
const mockDeviceId = "mockDeviceId";
let makeDeviceKeySpy: jest.SpyInstance;
let rsaGenerateKeyPairSpy: jest.SpyInstance;
let cryptoSvcGetEncKeySpy: jest.SpyInstance;
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
let encryptServiceEncryptSpy: jest.SpyInstance;
let appIdServiceGetAppIdSpy: jest.SpyInstance;
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
beforeEach(() => {
// Setup all spies and default return values for the happy path
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength) as CsprngArray;
mockUserSymKey = new SymmetricCryptoKey(mockUserSymKeyRandomBytes);
mockDeviceRsaKeyPair = [
new Uint8Array(deviceRsaKeyLength),
new Uint8Array(deviceRsaKeyLength),
];
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
mockDevicePublicKeyEncryptedUserSymKey = new EncString(
EncryptionType.Rsa2048_OaepSha1_B64,
"mockDevicePublicKeyEncryptedUserSymKey"
);
mockUserSymKeyEncryptedDevicePublicKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockUserSymKeyEncryptedDevicePublicKey"
);
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockDeviceKeyEncryptedDevicePrivateKey"
);
// TypeScript will allow calling private methods if the object is of type 'any'
makeDeviceKeySpy = jest
.spyOn(deviceCryptoService as any, "makeDeviceKey")
.mockResolvedValue(mockDeviceKey);
rsaGenerateKeyPairSpy = jest
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
.mockResolvedValue(mockDeviceRsaKeyPair);
cryptoSvcGetEncKeySpy = jest
.spyOn(cryptoService, "getEncKey")
.mockResolvedValue(mockUserSymKey);
cryptoSvcRsaEncryptSpy = jest
.spyOn(cryptoService, "rsaEncrypt")
.mockResolvedValue(mockDevicePublicKeyEncryptedUserSymKey);
encryptServiceEncryptSpy = jest
.spyOn(encryptService, "encrypt")
.mockImplementation((plainValue, key) => {
if (plainValue === mockDevicePublicKey && key === mockUserSymKey) {
return Promise.resolve(mockUserSymKeyEncryptedDevicePublicKey);
}
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
}
});
appIdServiceGetAppIdSpy = jest
.spyOn(appIdService, "getAppId")
.mockResolvedValue(mockDeviceId);
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
.mockResolvedValue(mockDeviceResponse);
});
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
const response = await deviceCryptoService.trustDevice();
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcGetEncKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
mockDeviceId,
mockDevicePublicKeyEncryptedUserSymKey.encryptedString,
mockUserSymKeyEncryptedDevicePublicKey.encryptedString,
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
);
expect(response).toBeInstanceOf(DeviceResponse);
expect(response).toEqual(mockDeviceResponse);
});
it("throws specific error if user symmetric key is not found", async () => {
// setup the spy to return null
cryptoSvcGetEncKeySpy.mockResolvedValue(null);
// check if the expected error is thrown
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
// reset the spy
cryptoSvcGetEncKeySpy.mockReset();
// setup the spy to return undefined
cryptoSvcGetEncKeySpy.mockResolvedValue(undefined);
// check if the expected error is thrown
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
});
const methodsToTestForErrorsOrInvalidReturns = [
{
method: "makeDeviceKey",
spy: () => makeDeviceKeySpy,
errorText: "makeDeviceKey error",
},
{
method: "rsaGenerateKeyPair",
spy: () => rsaGenerateKeyPairSpy,
errorText: "rsaGenerateKeyPair error",
},
{
method: "getEncKey",
spy: () => cryptoSvcGetEncKeySpy,
errorText: "getEncKey error",
},
{
method: "rsaEncrypt",
spy: () => cryptoSvcRsaEncryptSpy,
errorText: "rsaEncrypt error",
},
{
method: "encryptService.encrypt",
spy: () => encryptServiceEncryptSpy,
errorText: "encryptService.encrypt error",
},
];
describe.each(methodsToTestForErrorsOrInvalidReturns)(
"trustDevice error handling and invalid return testing",
({ method, spy, errorText }) => {
// ensures that error propagation works correctly
it(`throws an error if ${method} fails`, async () => {
const methodSpy = spy();
methodSpy.mockRejectedValue(new Error(errorText));
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(errorText);
});
test.each([null, undefined])(
`throws an error if ${method} returns %s`,
async (invalidValue) => {
const methodSpy = spy();
methodSpy.mockResolvedValue(invalidValue);
await expect(deviceCryptoService.trustDevice()).rejects.toThrow();
}
);
}
);
});
});
});

View File

@@ -1,64 +0,0 @@
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { Utils } from "../../platform/misc/utils";
import { ApiService } from "../api.service";
import { TrustedDeviceKeysRequest } from "./requests/trusted-device-keys.request";
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
const r = await this.apiService.send(
"GET",
"/devices/knowndevice",
null,
false,
true,
null,
(headers) => {
headers.set("X-Device-Identifier", deviceIdentifier);
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
}
);
return r as boolean;
}
/**
* Get device by identifier
* @param deviceIdentifier - client generated id (not device id in DB)
*/
async getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse> {
const r = await this.apiService.send(
"GET",
`/devices/identifier/${deviceIdentifier}`,
null,
true,
true
);
return new DeviceResponse(r);
}
async updateTrustedDeviceKeys(
deviceIdentifier: string,
devicePublicKeyEncryptedUserSymKey: string,
userSymKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
): Promise<DeviceResponse> {
const request = new TrustedDeviceKeysRequest(
devicePublicKeyEncryptedUserSymKey,
userSymKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey
);
const result = await this.apiService.send(
"PUT",
`/devices/${deviceIdentifier}/keys`,
request,
true,
true
);
return new DeviceResponse(result);
}
}

View File

@@ -0,0 +1,68 @@
import { Observable, defer, map } from "rxjs";
import { DevicesServiceAbstraction } from "../../abstractions/devices/devices.service.abstraction";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { DeviceView } from "../../abstractions/devices/views/device.view";
import { DevicesApiServiceAbstraction } from "../../auth/abstractions/devices-api.service.abstraction";
import { ListResponse } from "../../models/response/list.response";
/**
* @class DevicesServiceImplementation
* @implements {DevicesServiceAbstraction}
* @description Observable based data store service for Devices.
* note: defer is used to convert the promises to observables and to ensure
* that observables are created for each subscription
* (i.e., promsise --> observables are cold until subscribed to)
*/
export class DevicesServiceImplementation implements DevicesServiceAbstraction {
constructor(private devicesApiService: DevicesApiServiceAbstraction) {}
/**
* @description Gets the list of all devices.
*/
getDevices$(): Observable<Array<DeviceView>> {
return defer(() => this.devicesApiService.getDevices()).pipe(
map((deviceResponses: ListResponse<DeviceResponse>) => {
return deviceResponses.data.map((deviceResponse: DeviceResponse) => {
return new DeviceView(deviceResponse);
});
})
);
}
/**
* @description Gets the device with the specified identifier.
*/
getDeviceByIdentifier$(deviceIdentifier: string): Observable<DeviceView> {
return defer(() => this.devicesApiService.getDeviceByIdentifier(deviceIdentifier)).pipe(
map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse))
);
}
/**
* @description Checks if a device is known for a user by user's email and device's identifier.
*/
isDeviceKnownForUser$(email: string, deviceIdentifier: string): Observable<boolean> {
return defer(() => this.devicesApiService.getKnownDevice(email, deviceIdentifier));
}
/**
* @description Updates the keys for the specified device.
*/
updateTrustedDeviceKeys$(
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
): Observable<DeviceView> {
return defer(() =>
this.devicesApiService.updateTrustedDeviceKeys(
deviceIdentifier,
devicePublicKeyEncryptedUserKey,
userKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey
)
).pipe(map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse)));
}
}

View File

@@ -0,0 +1,146 @@
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { Policy } from "../../admin-console/models/domain/policy";
import { TokenService } from "../../auth/abstractions/token.service";
import { UserVerificationService } from "../../auth/abstractions/user-verification/user-verification.service.abstraction";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
describe("VaultTimeoutSettingsService", () => {
let cryptoService: MockProxy<CryptoService>;
let tokenService: MockProxy<TokenService>;
let policyService: MockProxy<PolicyService>;
let stateService: MockProxy<StateService>;
let userVerificationService: MockProxy<UserVerificationService>;
let service: VaultTimeoutSettingsService;
beforeEach(() => {
cryptoService = mock<CryptoService>();
tokenService = mock<TokenService>();
policyService = mock<PolicyService>();
stateService = mock<StateService>();
userVerificationService = mock<UserVerificationService>();
service = new VaultTimeoutSettingsService(
cryptoService,
tokenService,
policyService,
stateService,
userVerificationService
);
});
describe("availableVaultTimeoutActions$", () => {
it("always returns LogOut", async () => {
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.LogOut);
});
it("contains Lock when the user has a master password", async () => {
userVerificationService.hasMasterPassword.mockResolvedValue(true);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has a persistent PIN configured", async () => {
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(createEncString());
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has a transient/ephemeral PIN configured", async () => {
stateService.getProtectedPin.mockResolvedValue("some-key");
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has biometrics configured", async () => {
stateService.getBiometricUnlock.mockResolvedValue(true);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => {
userVerificationService.hasMasterPassword.mockResolvedValue(false);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
stateService.getProtectedPin.mockResolvedValue(null);
stateService.getBiometricUnlock.mockResolvedValue(false);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).not.toContain(VaultTimeoutAction.Lock);
});
});
describe("vaultTimeoutAction$", () => {
describe("given the user has a master password", () => {
it.each`
policy | userPreference | expected
${null} | ${null} | ${VaultTimeoutAction.Lock}
${null} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.LogOut}
${VaultTimeoutAction.LogOut} | ${null} | ${VaultTimeoutAction.LogOut}
${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut}
`(
"returns $expected when policy is $policy, and user preference is $userPreference",
async ({ policy, userPreference, expected }) => {
userVerificationService.hasMasterPassword.mockResolvedValue(true);
policyService.policyAppliesToUser.mockResolvedValue(policy === null ? false : true);
policyService.getAll.mockResolvedValue(
policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])
);
stateService.getVaultTimeoutAction.mockResolvedValue(userPreference);
const result = await firstValueFrom(service.vaultTimeoutAction$());
expect(result).toBe(expected);
}
);
});
describe("given the user does not have a master password", () => {
it.each`
unlockMethod | policy | userPreference | expected
${false} | ${null} | ${null} | ${VaultTimeoutAction.LogOut}
${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut}
${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.LogOut}
${true} | ${null} | ${null} | ${VaultTimeoutAction.LogOut}
${true} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock}
${true} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock}
${true} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock}
`(
"returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference",
async ({ unlockMethod, policy, userPreference, expected }) => {
stateService.getBiometricUnlock.mockResolvedValue(unlockMethod);
userVerificationService.hasMasterPassword.mockResolvedValue(false);
policyService.policyAppliesToUser.mockResolvedValue(policy === null ? false : true);
policyService.getAll.mockResolvedValue(
policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])
);
stateService.getVaultTimeoutAction.mockResolvedValue(userPreference);
const result = await firstValueFrom(service.vaultTimeoutAction$());
expect(result).toBe(expected);
}
);
});
});
});
function createEncString() {
return Symbol() as unknown as EncString;
}

View File

@@ -1,17 +1,28 @@
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { defer } from "rxjs";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../admin-console/enums";
import { TokenService } from "../../auth/abstractions/token.service";
import { UserVerificationService } from "../../auth/abstractions/user-verification/user-verification.service.abstraction";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service";
/**
* - DISABLED: No Pin set
* - PERSISTENT: Pin is set and survives client reset
* - TRANSIENT: Pin is set and requires password unlock after client reset
*/
export type PinLockType = "DISABLED" | "PERSISTANT" | "TRANSIENT";
export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private tokenService: TokenService,
private policyService: PolicyService,
private stateService: StateService
private stateService: StateService,
private userVerificationService: UserVerificationService
) {}
async setVaultTimeoutOptions(timeout: number, action: VaultTimeoutAction): Promise<void> {
@@ -41,21 +52,35 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
await this.tokenService.setClientId(clientId);
await this.tokenService.setClientSecret(clientSecret);
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
}
async isPinLockSet(): Promise<[boolean, boolean]> {
const protectedPin = await this.stateService.getProtectedPin();
const pinProtectedKey = await this.stateService.getEncryptedPinProtected();
return [protectedPin != null, pinProtectedKey != null];
availableVaultTimeoutActions$(userId?: string) {
return defer(() => this.getAvailableVaultTimeoutActions(userId));
}
async isBiometricLockSet(): Promise<boolean> {
return await this.stateService.getBiometricUnlock();
async isPinLockSet(userId?: string): Promise<PinLockType> {
// we can't check the protected pin for both because old accounts only
// used it for MP on Restart
const pinIsEnabled = !!(await this.stateService.getProtectedPin({ userId }));
const aUserKeyPinIsSet = !!(await this.stateService.getPinKeyEncryptedUserKey({ userId }));
const anOldUserKeyPinIsSet = !!(await this.stateService.getEncryptedPinProtected({ userId }));
if (aUserKeyPinIsSet || anOldUserKeyPinIsSet) {
return "PERSISTANT";
} else if (pinIsEnabled && !aUserKeyPinIsSet && !anOldUserKeyPinIsSet) {
return "TRANSIENT";
} else {
return "DISABLED";
}
}
async isBiometricLockSet(userId?: string): Promise<boolean> {
return await this.stateService.getBiometricUnlock({ userId });
}
async getVaultTimeout(userId?: string): Promise<number> {
const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId });
const vaultTimeout = await this.stateService.getVaultTimeout({ userId });
if (
await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)
@@ -68,9 +93,11 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
timeout = policy[0].data.minutes;
}
// TODO @jlf0dev: Can we move this somwhere else? Maybe add it to the initialization process?
// ( Apparently I'm the one that reviewed the original PR that added this :) )
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (vaultTimeout !== timeout) {
await this.stateService.setVaultTimeout(timeout, { userId: userId });
await this.stateService.setVaultTimeout(timeout, { userId });
}
return timeout;
@@ -79,22 +106,40 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
return vaultTimeout;
}
vaultTimeoutAction$(userId?: string) {
return defer(() => this.getVaultTimeoutAction(userId));
}
async getVaultTimeoutAction(userId?: string): Promise<VaultTimeoutAction> {
let vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId });
const availableActions = await this.getAvailableVaultTimeoutActions();
if (availableActions.length === 1) {
return availableActions[0];
}
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId });
if (
await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)
) {
const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId);
const action = policy[0].data.action;
if (action) {
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (action && vaultTimeoutAction !== action) {
await this.stateService.setVaultTimeoutAction(action, { userId: userId });
}
vaultTimeoutAction = action;
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (action && vaultTimeoutAction !== action) {
await this.stateService.setVaultTimeoutAction(action, { userId: userId });
}
if (action && availableActions.includes(action)) {
return action;
}
}
if (vaultTimeoutAction == null) {
// Depends on whether or not the user has a master password
const defaultValue = (await this.userVerificationService.hasMasterPassword())
? VaultTimeoutAction.Lock
: VaultTimeoutAction.LogOut;
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
await this.stateService.setVaultTimeoutAction(defaultValue, { userId: userId });
return defaultValue;
}
return vaultTimeoutAction === VaultTimeoutAction.LogOut
@@ -102,9 +147,23 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
: VaultTimeoutAction.Lock;
}
private async getAvailableVaultTimeoutActions(userId?: string): Promise<VaultTimeoutAction[]> {
const availableActions = [VaultTimeoutAction.LogOut];
const canLock =
(await this.userVerificationService.hasMasterPassword(userId)) ||
(await this.isPinLockSet(userId)) !== "DISABLED" ||
(await this.isBiometricLockSet(userId));
if (canLock) {
availableActions.push(VaultTimeoutAction.Lock);
}
return availableActions;
}
async clear(userId?: string): Promise<void> {
await this.stateService.setEverBeenUnlocked(false, { userId: userId });
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId });
await this.cryptoService.clearPinKeys(userId);
}
}

View File

@@ -1,10 +1,9 @@
import { firstValueFrom } from "rxjs";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { KeyConnectorService } from "../../auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
@@ -26,7 +25,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
protected platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private searchService: SearchService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
private authService: AuthService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
@@ -34,10 +32,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null
) {}
init(checkOnInterval: boolean) {
async init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3483)
await this.migrateKeyForNeverLockIfNeeded();
this.inited = true;
if (checkOnInterval) {
@@ -69,14 +69,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
return;
}
if (await this.keyConnectorService.getUsesKeyConnector()) {
const pinSet = await this.vaultTimeoutSettingsService.isPinLockSet();
const pinLock =
(pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) || pinSet[1];
if (!pinLock && !(await this.vaultTimeoutSettingsService.isBiometricLockSet())) {
await this.logOut(userId);
}
const availableActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$()
);
const supportsLock = availableActions.includes(VaultTimeoutAction.Lock);
if (!supportsLock) {
await this.logOut(userId);
}
if (userId == null || userId === (await this.stateService.getUserId())) {
@@ -85,12 +83,13 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
}
await this.stateService.setEverBeenUnlocked(true, { userId: userId });
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
await this.cryptoService.clearKey(false, userId);
await this.cryptoService.clearUserKey(false, userId);
await this.cryptoService.clearMasterKey(userId);
await this.cryptoService.clearOrgKeys(true, userId);
await this.cryptoService.clearKeyPair(true, userId);
await this.cryptoService.clearEncKey(true, userId);
await this.cipherService.clearCache(userId);
await this.collectionService.clearCache(userId);
@@ -133,9 +132,20 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
}
private async executeTimeoutAction(userId: string): Promise<void> {
const timeoutAction = await this.vaultTimeoutSettingsService.getVaultTimeoutAction(userId);
const timeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId)
);
timeoutAction === VaultTimeoutAction.LogOut
? await this.logOut(userId)
: await this.lock(userId);
}
private async migrateKeyForNeverLockIfNeeded(): Promise<void> {
const accounts = await firstValueFrom(this.stateService.accounts$);
for (const userId in accounts) {
if (userId != null) {
await this.cryptoService.migrateAutoKeyIfNeeded(userId);
}
}
}
}

View File

@@ -336,7 +336,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
}
async getHistory(): Promise<GeneratedPasswordHistory[]> {
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
return new Array<GeneratedPasswordHistory>();
}
@@ -356,7 +356,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
async addHistory(password: string): Promise<void> {
// Cannot add new history if no key is available
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
return;
}

View File

@@ -143,9 +143,9 @@ export class SendService implements InternalSendServiceAbstraction {
}
decSends = [];
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
throw new Error("No key.");
throw new Error("No user key found.");
}
const promises: Promise<any>[] = [];
@@ -249,7 +249,7 @@ export class SendService implements InternalSendServiceAbstraction {
const sends = Object.values(sendsMap || {}).map((f) => new Send(f));
this._sends.next(sends);
if (await this.cryptoService.hasKey()) {
if (await this.cryptoService.hasUserKey()) {
this._sendViews.next(await this.decryptSends(sends));
}
}

View File

@@ -3,8 +3,12 @@ import { mock, MockProxy } from "jest-mock-extended";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import {
OrgKey,
SymmetricCryptoKey,
UserKey,
} from "../../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../../platform/services/container.service";
import { AttachmentData } from "../../models/data/attachment.data";
import { Attachment } from "../../models/domain/attachment";
@@ -105,12 +109,12 @@ describe("Attachment", () => {
await attachment.decrypt(null, providedKey);
expect(cryptoService.getKeyForUserEncryption).not.toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey);
});
it("gets an organization key if required", async () => {
const orgKey = mock<SymmetricCryptoKey>();
const orgKey = mock<OrgKey>();
cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
await attachment.decrypt("orgId", null);
@@ -120,12 +124,12 @@ describe("Attachment", () => {
});
it("gets the user's decryption key if required", async () => {
const userKey = mock<SymmetricCryptoKey>();
cryptoService.getKeyForUserEncryption.mockResolvedValue(userKey);
const userKey = mock<UserKey>();
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
await attachment.decrypt(null, null);
expect(cryptoService.getKeyForUserEncryption).toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey);
});
});
@@ -136,8 +140,8 @@ describe("Attachment", () => {
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
const actual = Attachment.fromJSON({
key: "myKey",
fileName: "myFileName",
key: "myKey" as EncryptedString,
fileName: "myFileName" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -71,7 +71,7 @@ export class Attachment extends Domain {
const cryptoService = Utils.getContainerService().getCryptoService();
return orgId != null
? await cryptoService.getOrgKey(orgId)
: await cryptoService.getKeyForUserEncryption();
: await cryptoService.getUserKeyWithLegacySupport();
}
toAttachmentData(): AttachmentData {

View File

@@ -1,5 +1,5 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { CardData } from "../../../vault/models/data/card.data";
import { Card } from "../../models/domain/card";
@@ -76,12 +76,12 @@ describe("Card", () => {
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
const actual = Card.fromJSON({
cardholderName: "mockCardHolder",
brand: "mockBrand",
number: "mockNumber",
expMonth: "mockExpMonth",
expYear: "mockExpYear",
code: "mockCode",
cardholderName: "mockCardHolder" as EncryptedString,
brand: "mockBrand" as EncryptedString,
number: "mockNumber" as EncryptedString,
expMonth: "mockExpMonth" as EncryptedString,
expYear: "mockExpYear" as EncryptedString,
code: "mockCode" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -1,6 +1,6 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { FieldType } from "../../../enums";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { FieldData } from "../../models/data/field.data";
import { Field } from "../../models/domain/field";
@@ -67,8 +67,8 @@ describe("Field", () => {
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
const actual = Field.fromJSON({
name: "myName",
value: "myValue",
name: "myName" as EncryptedString,
value: "myValue" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -1,5 +1,5 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { FolderData } from "../../models/data/folder.data";
import { Folder } from "../../models/domain/folder";
@@ -47,7 +47,7 @@ describe("Folder", () => {
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
const actual = Folder.fromJSON({
revisionDate: revisionDate.toISOString(),
name: "name",
name: "name" as EncryptedString,
id: "id",
});

View File

@@ -1,5 +1,5 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { IdentityData } from "../../models/data/identity.data";
import { Identity } from "../../models/domain/identity";
@@ -137,24 +137,24 @@ describe("Identity", () => {
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
const actual = Identity.fromJSON({
firstName: "mockFirstName",
lastName: "mockLastName",
address1: "mockAddress1",
address2: "mockAddress2",
address3: "mockAddress3",
city: "mockCity",
company: "mockCompany",
country: "mockCountry",
email: "mockEmail",
licenseNumber: "mockLicenseNumber",
middleName: "mockMiddleName",
passportNumber: "mockPassportNumber",
phone: "mockPhone",
postalCode: "mockPostalCode",
ssn: "mockSsn",
state: "mockState",
title: "mockTitle",
username: "mockUsername",
firstName: "mockFirstName" as EncryptedString,
lastName: "mockLastName" as EncryptedString,
address1: "mockAddress1" as EncryptedString,
address2: "mockAddress2" as EncryptedString,
address3: "mockAddress3" as EncryptedString,
city: "mockCity" as EncryptedString,
company: "mockCompany" as EncryptedString,
country: "mockCountry" as EncryptedString,
email: "mockEmail" as EncryptedString,
licenseNumber: "mockLicenseNumber" as EncryptedString,
middleName: "mockMiddleName" as EncryptedString,
passportNumber: "mockPassportNumber" as EncryptedString,
phone: "mockPhone" as EncryptedString,
postalCode: "mockPostalCode" as EncryptedString,
ssn: "mockSsn" as EncryptedString,
state: "mockState" as EncryptedString,
title: "mockTitle" as EncryptedString,
username: "mockUsername" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -3,7 +3,7 @@ import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { mockEnc, mockFromJson } from "../../../../spec";
import { UriMatchType } from "../../../enums";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { LoginData } from "../../models/data/login.data";
import { Login } from "../../models/domain/login";
import { LoginUri } from "../../models/domain/login-uri";
@@ -108,22 +108,22 @@ describe("Login DTO", () => {
const actual = Login.fromJSON({
uris: ["loginUri1", "loginUri2"] as any,
username: "myUsername",
password: "myPassword",
username: "myUsername" as EncryptedString,
password: "myPassword" as EncryptedString,
passwordRevisionDate: passwordRevisionDate.toISOString(),
totp: "myTotp",
totp: "myTotp" as EncryptedString,
fido2Key: {
nonDiscoverableId: "keyId",
keyType: "keyType",
keyAlgorithm: "keyAlgorithm",
keyCurve: "keyCurve",
keyValue: "keyValue",
rpId: "rpId",
userHandle: "userHandle",
counter: "counter",
rpName: "rpName",
userName: "userName",
origin: "origin",
nonDiscoverableId: "keyId" as EncryptedString,
keyType: "keyType" as EncryptedString,
keyAlgorithm: "keyAlgorithm" as EncryptedString,
keyCurve: "keyCurve" as EncryptedString,
keyValue: "keyValue" as EncryptedString,
rpId: "rpId" as EncryptedString,
userHandle: "userHandle" as EncryptedString,
counter: "counter" as EncryptedString,
rpName: "rpName" as EncryptedString,
userName: "userName" as EncryptedString,
origin: "origin" as EncryptedString,
},
});

View File

@@ -1,5 +1,5 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { PasswordHistoryData } from "../../models/data/password-history.data";
import { Password } from "../../models/domain/password";
@@ -55,7 +55,7 @@ describe("Password", () => {
const lastUsedDate = new Date("2022-01-31T12:00:00.000Z");
const actual = Password.fromJSON({
password: "myPassword",
password: "myPassword" as EncryptedString,
lastUsedDate: lastUsedDate.toISOString(),
});

View File

@@ -9,7 +9,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { OrgKey, SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type";
@@ -118,11 +118,11 @@ describe("Cipher Service", () => {
describe("saveAttachmentRawWithServer()", () => {
it("should upload encrypted file contents with save attachments", async () => {
const fileName = "filename";
const fileData = new Uint8Array(10).buffer;
const fileData = new Uint8Array(10);
cryptoService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)))
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey)
);
cryptoService.makeEncKey.mockReturnValue(
cryptoService.makeDataEncKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)))
);
const spy = jest.spyOn(cipherFileUploadService, "upload");

View File

@@ -13,7 +13,11 @@ import { Utils } from "../../platform/misc/utils";
import Domain from "../../platform/models/domain/domain-base";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import {
OrgKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { CipherType } from "../enums/cipher-type";
@@ -326,14 +330,14 @@ export class CipherService implements CipherServiceAbstraction {
return await this.getDecryptedCipherCache();
}
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
throw new Error("No key.");
throw new Error("No user key found.");
}
const ciphers = await this.getAll();
const orgKeys = await this.cryptoService.getOrgKeys();
const userKey = await this.cryptoService.getKeyForUserEncryption();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
// Group ciphers by orgId or under 'null' for the user's ciphers
const grouped = ciphers.reduce((agg, c) => {
@@ -637,14 +641,17 @@ export class CipherService implements CipherServiceAbstraction {
async saveAttachmentRawWithServer(
cipher: Cipher,
filename: string,
data: ArrayBuffer,
data: Uint8Array,
admin = false
): Promise<Cipher> {
const key = await this.cryptoService.getOrgKey(cipher.organizationId);
const encFileName = await this.cryptoService.encrypt(filename, key);
let encKey: UserKey | OrgKey;
encKey = await this.cryptoService.getOrgKey(cipher.organizationId);
encKey ||= await this.cryptoService.getUserKeyWithLegacySupport();
const dataEncKey = await this.cryptoService.makeEncKey(key);
const encData = await this.cryptoService.encryptToBytes(new Uint8Array(data), dataEncKey[0]);
const dataEncKey = await this.cryptoService.makeDataEncKey(encKey);
const encFileName = await this.encryptService.encrypt(filename, encKey);
const encData = await this.encryptService.encryptToBytes(data, dataEncKey[0]);
const response = await this.cipherFileUploadService.upload(
cipher,
@@ -971,11 +978,15 @@ export class CipherService implements CipherServiceAbstraction {
const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse);
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, null);
const key = await this.cryptoService.getOrgKey(organizationId);
const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key);
const dataEncKey = await this.cryptoService.makeEncKey(key);
const encData = await this.cryptoService.encryptToBytes(decBuf, dataEncKey[0]);
let encKey: UserKey | OrgKey;
encKey = await this.cryptoService.getOrgKey(organizationId);
encKey ||= (await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey;
const dataEncKey = await this.cryptoService.makeDataEncKey(encKey);
const encFileName = await this.encryptService.encrypt(attachmentView.fileName, encKey);
const encData = await this.encryptService.encryptToBytes(new Uint8Array(decBuf), dataEncKey[0]);
const fd = new FormData();
try {

View File

@@ -79,7 +79,7 @@ export class CollectionService implements CollectionServiceAbstraction {
return decryptedCollections;
}
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
throw new Error("No key.");
}

View File

@@ -173,7 +173,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
this._folders.next(folders);
if (await this.cryptoService.hasKey()) {
if (await this.cryptoService.hasUserKey()) {
this._folderViews.next(await this.decryptFolders(folders));
}
}

View File

@@ -303,8 +303,8 @@ export class SyncService implements SyncServiceAbstraction {
throw new Error("Stamp has changed");
}
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(response.privateKey);
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.stateService.setAvatarColor(response.avatarColor);