mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-26 12:25:20 +01:00
Auth/[PM-1260] - Existing User - Login with Trusted Device (Flow 2) (#5775)
* PM-1378 - Refactor - StateSvc.getDeviceKey() must actually convert JSON obj into instance of SymmetricCryptoKey * TODO: BaseLoginDecryptionOptionsComponent - verify new user check doesn't improperly pick up key connector users * PM-1260 - Add new encrypted keys to TrustedDeviceUserDecryptionOptionResponse * PM-1260 - DeviceTrustCryptoSvc - decryptUserKeyWithDeviceKey: (1) update method to optionally accept deviceKey (2) Return null user key when no device key exists (3) decryption of user key now works in the happy path * PM-1260 - LoginStrategy - SaveAcctInfo - Must persist device key on new account entity created from IdTokenResponse for TDE to work * PM-1260 - SSO Login Strategy - setUserKey refactor - (1) Refactor existing logic into trySetUserKeyForKeyConnector + setUserKeyMasterKey call and (2) new trySetUserKeyWithDeviceKey method for TDE * PM-1260 - Refactor DeviceTrustCryptoService.decryptUserKeyWithDeviceKey(...) - Add try catch around decryption attempts which removes device key (and trust) on decryption failure + warn. * PM-1260 - Account - Add deviceKey to fromJSON * TODO: add device key tests to account keys * TODO: figure out state service issues with getDeviceKey or if they are an issue w/ the account deserialization as a whole * PM-1260 - Add test suite for decryptUserKeyWithDeviceKey * PM-1260 - Add interfaces for server responses for UserDecryptionOptions to make testing easier without having to use the dreaded any type. * PM-1260 - SSOLoginStrategy - SetUserKey - Add check looking for key connector url on user decryption options + comment about future deprecation of tokenResponse.keyConnectorUrl * PM-1260 - SSO Login Strategy Spec file - Add test suite for TDE set user key logic * PM-1260 - BaseLoginStrategy - add test to verify device key persists on login * PM-1260 - StateService - verified that settings persist properly post SSO and it's just device keys we must manually instantiate into SymmetricCryptoKeys * PM-1260 - Remove comment about being unable to feature flag auth service / login strategy code due to circ deps as we don't need to worry about it b/c of the way we've written the new logic to be additive. * PM-1260 - DevicesApiServiceImplementation - Update constructor to properly use abstraction for API service * PM-1260 - Browser - AuthService - (1) Add new, required service factories for auth svc and (2) Update auth svc creation in main.background with new deps * PM-1260 - CLI - Update AuthSvc deps * PM-1260 - Address PR feedback to add clarity / match conventions * PM-1260 - Resolving more minor PR feedback * PM-1260 - DeviceTrustCryptoService - remove debug warn * PM-1378 - DeviceTrustCryptoSvc - TrustDevice - Fix bug where we only partially encrypted the user key with the device public key b/c I incorrectly passed userKey.encKey (32 bytes) instead of userKey.key (64 bytes) to the rsaEncrypt function which lead to an encryption type mismatch when decrypting the user's private key with the 32 byte decrypted user key obtained after TDE login. (Updated happy path test to prevent this from happening again) * PM-1260 - AccountKeys tests - add tests for deviceKey persistence and deserialization * PM-1260 - DeviceTrustCryptoSvc Test - tweak verbiage per feedback * PM-1260 - DeviceTrustCryptoSvc - Test verbiage tweak part 2 * Update apps/browser/src/background/service-factories/devices-api-service.factory.ts per PR feedback Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
parent
511ca540b5
commit
2a5a30844d
@ -52,6 +52,7 @@ import {
|
||||
PasswordStrengthServiceInitOptions,
|
||||
} from "../../../tools/background/service_factories/password-strength-service.factory";
|
||||
|
||||
import { deviceTrustCryptoServiceFactory } from "./device-trust-crypto-service.factory";
|
||||
import {
|
||||
keyConnectorServiceFactory,
|
||||
KeyConnectorServiceInitOptions,
|
||||
@ -101,7 +102,8 @@ export function authServiceFactory(
|
||||
await i18nServiceFactory(cache, opts),
|
||||
await encryptServiceFactory(cache, opts),
|
||||
await passwordStrengthServiceFactory(cache, opts),
|
||||
await policyServiceFactory(cache, opts)
|
||||
await policyServiceFactory(cache, opts),
|
||||
await deviceTrustCryptoServiceFactory(cache, opts)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
|
||||
|
||||
import {
|
||||
DevicesApiServiceInitOptions,
|
||||
devicesApiServiceFactory,
|
||||
} from "../../../background/service-factories/devices-api-service.factory";
|
||||
import {
|
||||
AppIdServiceInitOptions,
|
||||
appIdServiceFactory,
|
||||
} from "../../../platform/background/service-factories/app-id-service.factory";
|
||||
import {
|
||||
CryptoFunctionServiceInitOptions,
|
||||
cryptoFunctionServiceFactory,
|
||||
} from "../../../platform/background/service-factories/crypto-function-service.factory";
|
||||
import {
|
||||
CryptoServiceInitOptions,
|
||||
cryptoServiceFactory,
|
||||
} from "../../../platform/background/service-factories/crypto-service.factory";
|
||||
import {
|
||||
EncryptServiceInitOptions,
|
||||
encryptServiceFactory,
|
||||
} from "../../../platform/background/service-factories/encrypt-service.factory";
|
||||
import {
|
||||
CachedServices,
|
||||
FactoryOptions,
|
||||
factory,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
StateServiceInitOptions,
|
||||
stateServiceFactory,
|
||||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
|
||||
type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions &
|
||||
CryptoFunctionServiceInitOptions &
|
||||
CryptoServiceInitOptions &
|
||||
EncryptServiceInitOptions &
|
||||
StateServiceInitOptions &
|
||||
AppIdServiceInitOptions &
|
||||
DevicesApiServiceInitOptions;
|
||||
|
||||
export function deviceTrustCryptoServiceFactory(
|
||||
cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices,
|
||||
opts: DeviceTrustCryptoServiceInitOptions
|
||||
): Promise<DeviceTrustCryptoServiceAbstraction> {
|
||||
return factory(
|
||||
cache,
|
||||
"deviceTrustCryptoService",
|
||||
opts,
|
||||
async () =>
|
||||
new DeviceTrustCryptoService(
|
||||
await cryptoFunctionServiceFactory(cache, opts),
|
||||
await cryptoServiceFactory(cache, opts),
|
||||
await encryptServiceFactory(cache, opts),
|
||||
await stateServiceFactory(cache, opts),
|
||||
await appIdServiceFactory(cache, opts),
|
||||
await devicesApiServiceFactory(cache, opts)
|
||||
)
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
|
||||
@ -16,12 +17,14 @@ import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common
|
||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
@ -59,6 +62,7 @@ import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/we
|
||||
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
|
||||
@ -195,6 +199,8 @@ export default class MainBackground {
|
||||
cipherContextMenuHandler: CipherContextMenuHandler;
|
||||
configService: ConfigServiceAbstraction;
|
||||
configApiService: ConfigApiServiceAbstraction;
|
||||
devicesApiService: DevicesApiServiceAbstraction;
|
||||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
|
||||
|
||||
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
||||
backgroundWindow = window;
|
||||
@ -385,6 +391,17 @@ export default class MainBackground {
|
||||
that.runtimeBackground.processMessage(message, that as any, null);
|
||||
};
|
||||
})();
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
this.deviceTrustCryptoService = new DeviceTrustCryptoService(
|
||||
this.cryptoFunctionService,
|
||||
this.cryptoService,
|
||||
this.encryptService,
|
||||
this.stateService,
|
||||
this.appIdService,
|
||||
this.devicesApiService
|
||||
);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
@ -400,7 +417,8 @@ export default class MainBackground {
|
||||
this.i18nService,
|
||||
this.encryptService,
|
||||
this.passwordStrengthService,
|
||||
this.policyService
|
||||
this.policyService,
|
||||
this.deviceTrustCryptoService
|
||||
);
|
||||
|
||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
|
||||
import {
|
||||
ApiServiceInitOptions,
|
||||
apiServiceFactory,
|
||||
} from "../../platform/background/service-factories/api-service.factory";
|
||||
import {
|
||||
FactoryOptions,
|
||||
CachedServices,
|
||||
factory,
|
||||
} from "../../platform/background/service-factories/factory-options";
|
||||
|
||||
type DevicesApiServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type DevicesApiServiceInitOptions = DevicesApiServiceFactoryOptions & ApiServiceInitOptions;
|
||||
|
||||
export function devicesApiServiceFactory(
|
||||
cache: { devicesApiService?: DevicesApiServiceAbstraction } & CachedServices,
|
||||
opts: DevicesApiServiceInitOptions
|
||||
): Promise<DevicesApiServiceAbstraction> {
|
||||
return factory(
|
||||
cache,
|
||||
"devicesApiService",
|
||||
opts,
|
||||
async () => new DevicesApiServiceImplementation(await apiServiceFactory(cache, opts))
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ import * as path from "path";
|
||||
import * as program from "commander";
|
||||
import * as jsdom from "jsdom";
|
||||
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
@ -12,7 +13,9 @@ import { OrganizationService } from "@bitwarden/common/admin-console/services/or
|
||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
@ -34,6 +37,7 @@ import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-m
|
||||
import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { OrganizationUserServiceImplementation } from "@bitwarden/common/services/organization-user/organization-user.service.implementation";
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
||||
@ -140,6 +144,8 @@ export class Main {
|
||||
organizationApiService: OrganizationApiServiceAbstraction;
|
||||
syncNotifierService: SyncNotifierService;
|
||||
sendApiService: SendApiService;
|
||||
devicesApiService: DevicesApiServiceAbstraction;
|
||||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@ -315,6 +321,16 @@ export class Main {
|
||||
this.stateService
|
||||
);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
this.deviceTrustCryptoService = new DeviceTrustCryptoService(
|
||||
this.cryptoFunctionService,
|
||||
this.cryptoService,
|
||||
this.encryptService,
|
||||
this.stateService,
|
||||
this.appIdService,
|
||||
this.devicesApiService
|
||||
);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
@ -330,7 +346,8 @@ export class Main {
|
||||
this.i18nService,
|
||||
this.encryptService,
|
||||
this.passwordStrengthService,
|
||||
this.policyService
|
||||
this.policyService,
|
||||
this.deviceTrustCryptoService
|
||||
);
|
||||
|
||||
const lockedCallback = async () =>
|
||||
|
@ -102,6 +102,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
||||
const accountDecryptionOptions: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
|
||||
// TODO: verify that this doesn't also pick up key connector users... can key connector users even get here?
|
||||
// see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response..
|
||||
// can we check if they have a user key or master key in crypto service? Would that be sufficient?
|
||||
if (
|
||||
!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
|
||||
!accountDecryptionOptions?.hasMasterPassword
|
||||
|
@ -247,6 +247,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
EncryptService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
DeviceTrustCryptoServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
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 {
|
||||
@ -11,9 +12,9 @@ export abstract class DeviceTrustCryptoServiceAbstraction {
|
||||
|
||||
trustDevice: () => Promise<DeviceResponse>;
|
||||
getDeviceKey: () => Promise<DeviceKey>;
|
||||
// TODO: update param types when available
|
||||
decryptUserKeyWithDeviceKey: (
|
||||
encryptedDevicePrivateKey: any,
|
||||
encryptedUserKey: any
|
||||
) => Promise<UserKey>;
|
||||
encryptedDevicePrivateKey: EncString,
|
||||
encryptedUserKey: EncString,
|
||||
deviceKey?: DeviceKey
|
||||
) => Promise<UserKey | null>;
|
||||
}
|
||||
|
@ -9,9 +9,16 @@ 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,
|
||||
@ -34,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";
|
||||
|
||||
@ -51,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,
|
||||
@ -64,7 +82,8 @@ 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,
|
||||
@ -79,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>;
|
||||
@ -149,8 +170,9 @@ describe("LogInStrategy", () => {
|
||||
) as MasterKey;
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
it("sets the local environment after a successful login with master password", async () => {
|
||||
const idTokenResponse = identityTokenResponseFactory();
|
||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
@ -174,11 +196,36 @@ describe("LogInStrategy", () => {
|
||||
refreshToken: refreshToken,
|
||||
},
|
||||
},
|
||||
keys: new AccountKeys(),
|
||||
decryptionOptions: acctDecryptionOptions,
|
||||
})
|
||||
);
|
||||
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;
|
||||
|
@ -9,6 +9,7 @@ import { StateService } from "../../platform/abstractions/state.service";
|
||||
import {
|
||||
Account,
|
||||
AccountDecryptionOptions,
|
||||
AccountKeys,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
} from "../../platform/models/domain/account";
|
||||
@ -104,12 +105,24 @@ 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;
|
||||
}
|
||||
|
||||
await this.stateService.addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: accountInformation.sub,
|
||||
userId,
|
||||
name: accountInformation.name,
|
||||
email: accountInformation.email,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
@ -126,6 +139,7 @@ export abstract class LogInStrategy {
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
},
|
||||
},
|
||||
keys: accountKeys,
|
||||
decryptionOptions: AccountDecryptionOptions.fromResponse(
|
||||
tokenResponse.userDecryptionOptions
|
||||
),
|
||||
|
@ -9,16 +9,19 @@ import { PlatformUtilsService } from "../../platform/abstractions/platform-utils
|
||||
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 { 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";
|
||||
@ -34,6 +37,7 @@ describe("SsoLogInStrategy", () => {
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
|
||||
|
||||
let ssoLogInStrategy: SsoLogInStrategy;
|
||||
let credentials: SsoLogInCredentials;
|
||||
@ -57,6 +61,7 @@ describe("SsoLogInStrategy", () => {
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
keyConnectorService = mock<KeyConnectorService>();
|
||||
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
|
||||
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
@ -72,7 +77,8 @@ describe("SsoLogInStrategy", () => {
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
keyConnectorService
|
||||
keyConnectorService,
|
||||
deviceTrustCryptoService
|
||||
);
|
||||
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
|
||||
});
|
||||
@ -110,6 +116,140 @@ describe("SsoLogInStrategy", () => {
|
||||
expect(cryptoService.setPrivateKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets master key encrypted user key for existing SSO users", async () => {
|
||||
// Arrange
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
// Act
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
// Assert
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
});
|
||||
|
||||
describe("Trusted Device Decryption", () => {
|
||||
const deviceKeyBytesLength = 64;
|
||||
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
const mockDeviceKey: DeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
const userKeyBytesLength = 64;
|
||||
const mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength).buffer as CsprngArray;
|
||||
const mockUserKey: UserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
|
||||
|
||||
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=";
|
||||
|
||||
const mockEncUserKey =
|
||||
"4.Xht6K9GA9jKcSNy4TaIvdj7f9+WsgQycs/HdkrJi33aC//roKkjf3UTGpdzFLxVP3WhyOVGyo9f2Jymf1MFPdpg7AuMnpGJlcrWLDbnPjOJo4x5gUwwBUmy3nFw6+wamyS1LRmrBPcv56yKpf80k5Q3hUrum8q9YS9m2I10vklX/TaB1YML0yo+K1feWUxg8vIx+vloxhUdkkysvcV5xU3R+AgYLrwvJS8TLL7Ug/P5HxinCaIroRrNe8xcv84vyVnzPFdXe0cfZ0cpcrm586LwfEXP2seeldO/bC51Uk/mudeSALJURPC64f5ch2cOvk48GOTapGnssCqr6ky5yFw==";
|
||||
|
||||
const userDecryptionOptsServerResponseWithTdeOption: IUserDecryptionOptionsServerResponse = {
|
||||
HasMasterPassword: true,
|
||||
TrustedDeviceOption: {
|
||||
HasAdminApproval: true,
|
||||
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(() => {
|
||||
|
@ -5,6 +5,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 { 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 +35,8 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
private keyConnectorService: KeyConnectorService
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
cryptoService,
|
||||
@ -80,37 +82,66 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
}
|
||||
|
||||
protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (!newSsoUser) {
|
||||
// TODO: check if TDE feature flag enabled and if token response account decryption options has TDE
|
||||
// and then if id token response has required device keys
|
||||
// DevicePublicKey(UserKey)
|
||||
// UserKey(DevicePublicKey)
|
||||
// DeviceKey(DevicePrivateKey)
|
||||
|
||||
// Once we have device keys coming back on id token response we can use this code
|
||||
// const userKey = await this.deviceCryptoService.decryptUserKeyWithDeviceKey(
|
||||
// encryptedDevicePrivateKey,
|
||||
// encryptedUserKey
|
||||
// );
|
||||
// await this.cryptoService.setUserKey(userKey);
|
||||
|
||||
// TODO: also admin approval request existence check should go here b/c that can give us a decrypted user key to set
|
||||
// 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?)
|
||||
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(tokenResponse.key);
|
||||
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
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);
|
||||
}
|
||||
// If new user, return b/c we can't set the user key yet
|
||||
if (tokenResponse.key === null) {
|
||||
return;
|
||||
}
|
||||
// Existing user; proceed
|
||||
|
||||
// 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(tokenResponse.key);
|
||||
|
||||
// TODO: also admin approval request existence check should go here b/c that can give us a decrypted user key to set
|
||||
// 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?)
|
||||
|
||||
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
||||
|
||||
// Note: TDE and key connector are mutually exclusive
|
||||
if (userDecryptionOptions?.trustedDeviceOption) {
|
||||
await this.trySetUserKeyWithDeviceKey(tokenResponse);
|
||||
} else if (
|
||||
// TODO: remove tokenResponse.keyConnectorUrl when it's deprecated
|
||||
tokenResponse.keyConnectorUrl ||
|
||||
userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl
|
||||
) {
|
||||
await this.trySetUserKeyWithMasterKey();
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
export interface IKeyConnectorUserDecryptionOptionServerResponse {
|
||||
KeyConnectorUrl: string;
|
||||
}
|
||||
|
||||
export class KeyConnectorUserDecryptionOptionResponse extends BaseResponse {
|
||||
keyConnectorUrl: string;
|
||||
|
||||
constructor(response: any) {
|
||||
constructor(response: IKeyConnectorUserDecryptionOptionServerResponse) {
|
||||
super(response);
|
||||
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
|
||||
}
|
||||
|
@ -1,10 +1,26 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
import { EncString } from "../../../../platform/models/domain/enc-string";
|
||||
|
||||
export interface ITrustedDeviceUserDecryptionOptionServerResponse {
|
||||
HasAdminApproval: boolean;
|
||||
EncryptedPrivateKey?: string;
|
||||
EncryptedUserKey?: string;
|
||||
}
|
||||
|
||||
export class TrustedDeviceUserDecryptionOptionResponse extends BaseResponse {
|
||||
hasAdminApproval: boolean;
|
||||
encryptedPrivateKey: EncString;
|
||||
encryptedUserKey: EncString;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.hasAdminApproval = this.getResponseProperty("HasAdminApproval");
|
||||
|
||||
if (response.EncryptedPrivateKey) {
|
||||
this.encryptedPrivateKey = new EncString(this.getResponseProperty("EncryptedPrivateKey"));
|
||||
}
|
||||
if (response.EncryptedUserKey) {
|
||||
this.encryptedUserKey = new EncString(this.getResponseProperty("EncryptedUserKey"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,26 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
import { KeyConnectorUserDecryptionOptionResponse } from "./key-connector-user-decryption-option.response";
|
||||
import { TrustedDeviceUserDecryptionOptionResponse } from "./trusted-device-user-decryption-option.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: any) {
|
||||
constructor(response: IUserDecryptionOptionsServerResponse) {
|
||||
super(response);
|
||||
|
||||
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
|
||||
|
@ -19,6 +19,7 @@ import { Utils } from "../../platform/misc/utils";
|
||||
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
|
||||
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 +104,8 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
protected i18nService: I18nService,
|
||||
protected encryptService: EncryptService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected policyService: PolicyService
|
||||
protected policyService: PolicyService,
|
||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
|
||||
) {}
|
||||
|
||||
async logIn(
|
||||
@ -149,7 +151,8 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.keyConnectorService
|
||||
this.keyConnectorService,
|
||||
this.deviceTrustCryptoService
|
||||
);
|
||||
break;
|
||||
case AuthenticationType.UserApi:
|
||||
|
@ -5,6 +5,7 @@ import { CryptoFunctionService } from "../../platform/abstractions/crypto-functi
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.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,
|
||||
@ -58,7 +59,7 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
||||
deviceKeyEncryptedDevicePrivateKey,
|
||||
] = await Promise.all([
|
||||
// Encrypt user key with the DevicePublicKey
|
||||
this.cryptoService.rsaEncrypt(userKey.encKey, devicePublicKey),
|
||||
this.cryptoService.rsaEncrypt(userKey.key, devicePublicKey),
|
||||
|
||||
// Encrypt devicePublicKey with user key
|
||||
this.encryptService.encrypt(devicePublicKey, userKey),
|
||||
@ -85,7 +86,7 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
||||
return await this.stateService.getDeviceKey();
|
||||
}
|
||||
|
||||
private async setDeviceKey(deviceKey: DeviceKey): Promise<void> {
|
||||
private async setDeviceKey(deviceKey: DeviceKey | null): Promise<void> {
|
||||
await this.stateService.setDeviceKey(deviceKey);
|
||||
}
|
||||
|
||||
@ -97,28 +98,38 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
|
||||
return deviceKey;
|
||||
}
|
||||
|
||||
// TODO: add proper types to parameters once we have them coming down from server
|
||||
// TODO: add tests for this method
|
||||
async decryptUserKeyWithDeviceKey(
|
||||
encryptedDevicePrivateKey: any,
|
||||
encryptedUserKey: any
|
||||
): Promise<UserKey> {
|
||||
// get device key
|
||||
const existingDeviceKey = await this.getDeviceKey();
|
||||
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 (!existingDeviceKey) {
|
||||
// TODO: not sure what to do here
|
||||
if (!deviceKey) {
|
||||
// User doesn't have a device key anymore so device is untrusted
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// attempt to decrypt encryptedDevicePrivateKey with device key
|
||||
const devicePrivateKey = await this.encryptService.decryptToBytes(
|
||||
encryptedDevicePrivateKey,
|
||||
existingDeviceKey
|
||||
);
|
||||
// Attempt to decrypt encryptedUserDataKey with devicePrivateKey
|
||||
const userKey = await this.cryptoService.rsaDecrypt(encryptedUserKey, devicePrivateKey);
|
||||
return new SymmetricCryptoKey(userKey) as UserKey;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -262,6 +262,11 @@ describe("deviceTrustCryptoService", () => {
|
||||
expect(cryptoSvcGetUserKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// RsaEncrypt must be called w/ a user key array buffer of 64 bytes
|
||||
const userKeyKey: ArrayBuffer = cryptoSvcRsaEncryptSpy.mock.calls[0][0];
|
||||
expect(userKeyKey.byteLength).toBe(64);
|
||||
|
||||
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
|
||||
@ -346,6 +351,108 @@ describe("deviceTrustCryptoService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// TOOD Add tests for decryptUserKeyWithDeviceKey when types are available
|
||||
describe("decryptUserKeyWithDeviceKey", () => {
|
||||
let mockDeviceKey: DeviceKey;
|
||||
let mockEncryptedDevicePrivateKey: EncString;
|
||||
let mockEncryptedUserKey: EncString;
|
||||
let mockUserKey: UserKey;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
const mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength).buffer 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).buffer);
|
||||
const rsaDecryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaDecrypt")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength).buffer);
|
||||
|
||||
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).buffer);
|
||||
const rsaDecryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaDecrypt")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength).buffer);
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -149,6 +149,7 @@ export class AccountKeys {
|
||||
return Object.assign(new AccountKeys(), {
|
||||
userKey: SymmetricCryptoKey.fromJSON(obj?.userKey),
|
||||
masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey),
|
||||
deviceKey: SymmetricCryptoKey.fromJSON(obj?.deviceKey),
|
||||
cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey),
|
||||
cryptoSymmetricKey: EncryptionPair.fromJSON(
|
||||
obj?.cryptoSymmetricKey,
|
||||
|
@ -1302,7 +1302,14 @@ 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> {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { DeviceType } from "../../enums";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { ApiService } from "../api.service";
|
||||
|
||||
import { TrustedDeviceKeysRequest } from "./requests/trusted-device-keys.request";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user