From 91ac281da0b018ba46572cf199b8cc535fb0bfe7 Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Thu, 1 Jun 2023 08:56:17 -0400 Subject: [PATCH] migrate login strategies to new key model - decrypt and set user symmetric key if Master Key is available - rename keys where applicable - update unit tests --- .../src/auth/abstractions/auth.service.ts | 4 +- .../abstractions/key-connector.service.ts | 2 +- .../login-strategies/login.strategy.spec.ts | 36 ++++-- .../auth/login-strategies/login.strategy.ts | 41 ++++--- .../password-login.strategy.spec.ts | 36 ++++-- .../password-login.strategy.ts | 41 +++++-- .../passwordless-login.strategy.spec.ts | 103 ++++++++++++++++++ .../passwordless-login.strategy.ts | 49 ++++++--- .../sso-login.strategy.spec.ts | 72 ++++++++---- .../login-strategies/sso-login.strategy.ts | 51 +++++++-- .../user-api-login.strategy.spec.ts | 41 ++++++- .../user-api-login.strategy.ts | 34 ++++-- .../auth/models/domain/log-in-credentials.ts | 4 +- libs/common/src/auth/services/auth.service.ts | 4 +- .../auth/services/key-connector.service.ts | 13 ++- 15 files changed, 410 insertions(+), 121 deletions(-) create mode 100644 libs/common/src/auth/login-strategies/passwordless-login.strategy.spec.ts diff --git a/libs/common/src/auth/abstractions/auth.service.ts b/libs/common/src/auth/abstractions/auth.service.ts index 231521382f..b6978c54c5 100644 --- a/libs/common/src/auth/abstractions/auth.service.ts +++ b/libs/common/src/auth/abstractions/auth.service.ts @@ -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; logOut: (callback: () => void) => void; - makePreloginKey: (masterPassword: string, email: string) => Promise; + makePreloginKey: (masterPassword: string, email: string) => Promise; authingWithUserApiKey: () => boolean; authingWithSso: () => boolean; authingWithPassword: () => boolean; diff --git a/libs/common/src/auth/abstractions/key-connector.service.ts b/libs/common/src/auth/abstractions/key-connector.service.ts index 0c4426a683..2d80a87e8c 100644 --- a/libs/common/src/auth/abstractions/key-connector.service.ts +++ b/libs/common/src/auth/abstractions/key-connector.service.ts @@ -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; + getAndSetMasterKey: (url?: string) => Promise; getManagingOrganization: () => Promise; getUsesKeyConnector: () => Promise; migrateUser: () => Promise; diff --git a/libs/common/src/auth/login-strategies/login.strategy.spec.ts b/libs/common/src/auth/login-strategies/login.strategy.spec.ts index ed958af6c7..3a6d84bf90 100644 --- a/libs/common/src/auth/login-strategies/login.strategy.spec.ts +++ b/libs/common/src/auth/login-strategies/login.strategy.spec.ts @@ -15,6 +15,12 @@ import { PasswordStrengthService, PasswordStrengthServiceAbstraction, } from "../../tools/password-strength"; +import { + MasterKey, + SymmetricCryptoKey, + UserSymKey, +} from "../../platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "../../types/csprng"; import { AuthService } from "../abstractions/auth.service"; import { TokenService } from "../abstractions/token.service"; import { TwoFactorService } from "../abstractions/two-factor.service"; @@ -37,7 +43,7 @@ const masterPassword = "password"; const deviceId = Utils.newGuid(); const accessToken = "ACCESS_TOKEN"; const refreshToken = "REFRESH_TOKEN"; -const encKey = "ENC_KEY"; +const userSymKey = "USER_SYM_KEY"; const privateKey = "PRIVATE_KEY"; const captchaSiteKey = "CAPTCHA_SITE_KEY"; const kdf = 0; @@ -64,7 +70,7 @@ export function identityTokenResponseFactory( ForcePasswordReset: false, Kdf: kdf, KdfIterations: kdfIterations, - Key: encKey, + Key: userSymKey, PrivateKey: privateKey, ResetMasterPassword: false, access_token: accessToken, @@ -129,6 +135,20 @@ describe("LogInStrategy", () => { }); describe("base class", () => { + const userSymKeyBytesLength = 64; + const masterKeyBytesLength = 64; + let userSymKey: UserSymKey; + let masterKey: MasterKey; + + beforeEach(() => { + userSymKey = new SymmetricCryptoKey( + new Uint8Array(userSymKeyBytesLength).buffer as CsprngArray + ) as UserSymKey; + masterKey = new SymmetricCryptoKey( + new Uint8Array(masterKeyBytesLength).buffer as CsprngArray + ) as MasterKey; + }); + it("sets the local environment after a successful login", async () => { apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); @@ -156,8 +176,6 @@ describe("LogInStrategy", () => { }, }) ); - expect(cryptoService.setEncKey).toHaveBeenCalledWith(encKey); - expect(cryptoService.setEncPrivateKey).toHaveBeenCalledWith(privateKey); expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); @@ -187,6 +205,8 @@ describe("LogInStrategy", () => { }); apiService.postIdentityToken.mockResolvedValue(tokenResponse); + cryptoService.getMasterKey.mockResolvedValue(masterKey); + cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey); const result = await passwordLogInStrategy.logIn(credentials); @@ -204,13 +224,15 @@ describe("LogInStrategy", () => { cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); apiService.postIdentityToken.mockResolvedValue(tokenResponse); + cryptoService.getMasterKey.mockResolvedValue(masterKey); + cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey); 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] ); diff --git a/libs/common/src/auth/login-strategies/login.strategy.ts b/libs/common/src/auth/login-strategies/login.strategy.ts index fbfa912f43..5561ed92de 100644 --- a/libs/common/src/auth/login-strategies/login.strategy.ts +++ b/libs/common/src/auth/login-strategies/login.strategy.ts @@ -53,9 +53,6 @@ export abstract class LogInStrategy { | PasswordlessLogInCredentials ): Promise; - // The user key comes from different sources depending on the login strategy - protected abstract setUserKey(response: IdentityTokenResponse): Promise; - async logInTwoFactor( twoFactor: TokenTwoFactorRequest, captchaResponse: string = null @@ -141,22 +138,34 @@ export abstract class LogInStrategy { 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; + + protected abstract setUserKey(response: IdentityTokenResponse): Promise; + + protected abstract setPrivateKey(response: IdentityTokenResponse): Promise; + + 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 { const result = new AuthResult(); result.twoFactorProviders = response.twoFactorProviders2; @@ -173,14 +182,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); - } - } } diff --git a/libs/common/src/auth/login-strategies/password-login.strategy.spec.ts b/libs/common/src/auth/login-strategies/password-login.strategy.spec.ts index 9e3ff7da68..abb1d5ca82 100644 --- a/libs/common/src/auth/login-strategies/password-login.strategy.spec.ts +++ b/libs/common/src/auth/login-strategies/password-login.strategy.spec.ts @@ -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 { PasswordStrengthService, PasswordStrengthServiceAbstraction, } from "../../tools/password-strength"; +import { + MasterKey, + SymmetricCryptoKey, + UserSymKey, +} from "../../platform/models/domain/symmetric-crypto-key"; +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(); @@ -76,7 +83,7 @@ describe("PasswordLogInStrategy", () => { appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeToken.mockResolvedValue({}); - authService.makePreloginKey.mockResolvedValue(preloginKey); + authService.makePreloginKey.mockResolvedValue(masterKey); cryptoService.hashPassword .calledWith(masterPassword, expect.anything(), undefined) @@ -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,25 @@ describe("PasswordLogInStrategy", () => { ); }); - it("sets the local environment after a successful login", async () => { + it("sets keys after a successful authentication", async () => { + const userSymKey = new SymmetricCryptoKey( + new Uint8Array(64).buffer as CsprngArray + ) as UserSymKey; + + cryptoService.getMasterKey.mockResolvedValue(masterKey); + cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey); + await passwordLogInStrategy.logIn(credentials); - expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey); + expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword); + expect(cryptoService.setUserSymKeyMasterKey).toHaveBeenCalledWith(tokenResponse.key); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(userSymKey); + 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); diff --git a/libs/common/src/auth/login-strategies/password-login.strategy.ts b/libs/common/src/auth/login-strategies/password-login.strategy.ts index 41f7c30ec7..f4c2da37b3 100644 --- a/libs/common/src/auth/login-strategies/password-login.strategy.ts +++ b/libs/common/src/auth/login-strategies/password-login.strategy.ts @@ -8,8 +8,8 @@ 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 { PasswordStrengthServiceAbstraction } from "../../tools/password-strength"; +import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key"; import { AuthService } from "../abstractions/auth.service"; import { TokenService } from "../abstractions/token.service"; import { TwoFactorService } from "../abstractions/two-factor.service"; @@ -36,7 +36,7 @@ export class PasswordLogInStrategy extends LogInStrategy { tokenRequest: PasswordTokenRequest; private localHashedPassword: string; - private key: SymmetricCryptoKey; + 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 { @@ -96,18 +91,18 @@ 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( masterPassword, - this.key, + this.masterKey, HashPurpose.LocalAuthorization ); - const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key); + const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.masterKey); this.tokenRequest = new PasswordTokenRequest( email, @@ -118,6 +113,7 @@ export class PasswordLogInStrategy extends LogInStrategy { ); 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.setKeyHash(this.localHashedPassword); + } + + protected override async setUserKey(response: IdentityTokenResponse): Promise { + await this.cryptoService.setUserSymKeyMasterKey(response.key); + + const masterKey = await this.cryptoService.getMasterKey(); + if (masterKey) { + const userKey = await this.cryptoService.decryptUserSymKeyWithMasterKey(masterKey); + await this.cryptoService.setUserKey(userKey); + } + } + + protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + await this.cryptoService.setPrivateKey( + response.privateKey ?? (await this.createKeyPairForOldAccount()) + ); + } + private getMasterPasswordPolicyOptionsFromResponse( response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse ): MasterPasswordPolicyOptions { diff --git a/libs/common/src/auth/login-strategies/passwordless-login.strategy.spec.ts b/libs/common/src/auth/login-strategies/passwordless-login.strategy.spec.ts new file mode 100644 index 0000000000..5b738f2d0a --- /dev/null +++ b/libs/common/src/auth/login-strategies/passwordless-login.strategy.spec.ts @@ -0,0 +1,103 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { ApiService } from "../../abstractions/api.service"; +import { AppIdService } from "../../abstractions/appId.service"; +import { CryptoService } from "../../abstractions/crypto.service"; +import { LogService } from "../../abstractions/log.service"; +import { MessagingService } from "../../abstractions/messaging.service"; +import { PlatformUtilsService } from "../../abstractions/platformUtils.service"; +import { StateService } from "../../abstractions/state.service"; +import { Utils } from "../../misc/utils"; +import { + MasterKey, + SymmetricCryptoKey, + UserSymKey, +} from "../../models/domain/symmetric-crypto-key"; +import { CsprngArray } from "../../types/csprng"; +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("SsoLogInStrategy", () => { + let cryptoService: MockProxy; + let apiService: MockProxy; + let tokenService: MockProxy; + let appIdService: MockProxy; + let platformUtilsService: MockProxy; + let messagingService: MockProxy; + let logService: MockProxy; + let stateService: MockProxy; + let twoFactorService: MockProxy; + + 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 decKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; + const localPasswordHash = "LOCAL_PASSWORD_HASH"; + + beforeEach(async () => { + cryptoService = mock(); + apiService = mock(); + tokenService = mock(); + appIdService = mock(); + platformUtilsService = mock(); + messagingService = mock(); + logService = mock(); + stateService = mock(); + twoFactorService = mock(); + + tokenService.getTwoFactorToken.mockResolvedValue(null); + appIdService.getAppId.mockResolvedValue(deviceId); + tokenService.decodeToken.mockResolvedValue({}); + + passwordlessLoginStrategy = new PasswordlessLogInStrategy( + cryptoService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + twoFactorService + ); + credentials = new PasswordlessLogInCredentials( + email, + accessCode, + authRequestId, + decKey, + localPasswordHash + ); + + tokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + }); + + it("sets keys after a successful authentication", async () => { + const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; + const userSymKey = new SymmetricCryptoKey( + new Uint8Array(64).buffer as CsprngArray + ) as UserSymKey; + + cryptoService.getMasterKey.mockResolvedValue(masterKey); + cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey); + + await passwordlessLoginStrategy.logIn(credentials); + + expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localPasswordHash); + expect(cryptoService.setUserSymKeyMasterKey).toHaveBeenCalledWith(tokenResponse.key); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(userSymKey); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + }); +}); diff --git a/libs/common/src/auth/login-strategies/passwordless-login.strategy.ts b/libs/common/src/auth/login-strategies/passwordless-login.strategy.ts index 382c55f06e..b0db957f1b 100644 --- a/libs/common/src/auth/login-strategies/passwordless-login.strategy.ts +++ b/libs/common/src/auth/login-strategies/passwordless-login.strategy.ts @@ -5,13 +5,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 { AuthService } from "../abstractions/auth.service"; 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"; @@ -40,8 +40,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy { messagingService: MessagingService, logService: LogService, stateService: StateService, - twoFactorService: TwoFactorService, - private authService: AuthService + twoFactorService: TwoFactorService ) { super( cryptoService, @@ -56,20 +55,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 { - 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 +70,33 @@ export class PasswordlessLogInStrategy extends LogInStrategy { const [authResult] = await this.startLogIn(); return authResult; } + + override async logInTwoFactor( + twoFactor: TokenTwoFactorRequest, + captchaResponse: string + ): Promise { + this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken; + return super.logInTwoFactor(twoFactor); + } + + protected override async setMasterKey(response: IdentityTokenResponse) { + await this.cryptoService.setMasterKey(this.passwordlessCredentials.decKey); + await this.cryptoService.setKeyHash(this.passwordlessCredentials.localPasswordHash); + } + + protected override async setUserKey(response: IdentityTokenResponse): Promise { + await this.cryptoService.setUserSymKeyMasterKey(response.key); + + const masterKey = await this.cryptoService.getMasterKey(); + if (masterKey) { + const userKey = await this.cryptoService.decryptUserSymKeyWithMasterKey(masterKey); + await this.cryptoService.setUserKey(userKey); + } + } + + protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + await this.cryptoService.setPrivateKey( + response.privateKey ?? (await this.createKeyPairForOldAccount()) + ); + } } diff --git a/libs/common/src/auth/login-strategies/sso-login.strategy.spec.ts b/libs/common/src/auth/login-strategies/sso-login.strategy.spec.ts index e0225fcacc..98c2949f5c 100644 --- a/libs/common/src/auth/login-strategies/sso-login.strategy.spec.ts +++ b/libs/common/src/auth/login-strategies/sso-login.strategy.spec.ts @@ -8,10 +8,17 @@ 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, + UserSymKey, +} 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"; import { SsoLogInCredentials } from "../models/domain/log-in-credentials"; +import { IdentityTokenResponse } from "../models/response/identity-token.response"; import { identityTokenResponseFactory } from "./login.strategy.spec"; import { SsoLogInStrategy } from "./sso-login.strategy"; @@ -98,33 +105,60 @@ 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 () => { - const tokenResponse = identityTokenResponseFactory(); - tokenResponse.keyConnectorUrl = keyConnectorUrl; + describe("Key Connector", () => { + let tokenResponse: IdentityTokenResponse; + beforeEach(() => { + tokenResponse = identityTokenResponseFactory(); + tokenResponse.keyConnectorUrl = keyConnectorUrl; + }); - apiService.postIdentityToken.mockResolvedValue(tokenResponse); + 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; - await ssoLogInStrategy.logIn(credentials); + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + cryptoService.getMasterKey.mockResolvedValue(masterKey); - expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl); - }); + await ssoLogInStrategy.logIn(credentials); - it("converts new SSO user to Key Connector on first login", async () => { - const tokenResponse = identityTokenResponseFactory(); - tokenResponse.keyConnectorUrl = keyConnectorUrl; - tokenResponse.key = null; + expect(keyConnectorService.getAndSetMasterKey).toHaveBeenCalledWith(keyConnectorUrl); + }); - apiService.postIdentityToken.mockResolvedValue(tokenResponse); + it("converts new SSO user to Key Connector on first login", async () => { + tokenResponse.key = null; - await ssoLogInStrategy.logIn(credentials); + apiService.postIdentityToken.mockResolvedValue(tokenResponse); - expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith( - tokenResponse, - ssoOrgId - ); + await ssoLogInStrategy.logIn(credentials); + + expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith( + tokenResponse, + ssoOrgId + ); + }); + + it("decrypts and sets the user symmetric key if Key Connector is enabled", async () => { + const userSymKey = new SymmetricCryptoKey( + new Uint8Array(64).buffer as CsprngArray + ) as UserSymKey; + const masterKey = new SymmetricCryptoKey( + new Uint8Array(64).buffer as CsprngArray + ) as MasterKey; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + cryptoService.getMasterKey.mockResolvedValue(masterKey); + cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey); + + await ssoLogInStrategy.logIn(credentials); + + expect(cryptoService.decryptUserSymKeyWithMasterKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(userSymKey); + }); }); }); diff --git a/libs/common/src/auth/login-strategies/sso-login.strategy.ts b/libs/common/src/auth/login-strategies/sso-login.strategy.ts index c77aa16841..be5435e3a6 100644 --- a/libs/common/src/auth/login-strategies/sso-login.strategy.ts +++ b/libs/common/src/auth/login-strategies/sso-login.strategy.ts @@ -49,18 +49,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 +66,43 @@ export class SsoLogInStrategy extends LogInStrategy { return ssoAuthResult; } + + protected override async setMasterKey(tokenResponse: IdentityTokenResponse) { + const newSsoUser = tokenResponse.key == null; + + if (tokenResponse.keyConnectorUrl != null) { + if (!newSsoUser) { + await this.keyConnectorService.getAndSetMasterKey(tokenResponse.keyConnectorUrl); + } else { + await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId); + } + } + } + + protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise { + const newSsoUser = tokenResponse.key == null; + + if (!newSsoUser) { + await this.cryptoService.setUserSymKeyMasterKey(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.decryptUserSymKeyWithMasterKey(masterKey); + await this.cryptoService.setUserKey(userKey); + } + } + } + + protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise { + const newSsoUser = tokenResponse.key == null; + + if (!newSsoUser) { + await this.cryptoService.setPrivateKey( + tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount()) + ); + } + } } diff --git a/libs/common/src/auth/login-strategies/user-api-login.strategy.spec.ts b/libs/common/src/auth/login-strategies/user-api-login.strategy.spec.ts index 92cae9d98c..fc918cbc5e 100644 --- a/libs/common/src/auth/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/common/src/auth/login-strategies/user-api-login.strategy.spec.ts @@ -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, + UserSymKey, +} 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 symmetric key and private key from the identity token response", async () => { + const tokenResponse = identityTokenResponseFactory(); + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + await apiLogInStrategy.logIn(credentials); + + expect(cryptoService.setUserSymKeyMasterKey).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,26 @@ describe("UserApiLogInStrategy", () => { await apiLogInStrategy.logIn(credentials); - expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl); + expect(keyConnectorService.getAndSetMasterKey).toHaveBeenCalledWith(keyConnectorUrl); + }); + + it("decrypts and sets the user symmetric key if Key Connector is enabled", async () => { + const userSymKey = new SymmetricCryptoKey( + new Uint8Array(64).buffer as CsprngArray + ) as UserSymKey; + 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.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey); + + await apiLogInStrategy.logIn(credentials); + + expect(cryptoService.decryptUserSymKeyWithMasterKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(userSymKey); }); }); diff --git a/libs/common/src/auth/login-strategies/user-api-login.strategy.ts b/libs/common/src/auth/login-strategies/user-api-login.strategy.ts index 967856ddd3..fdc57cbc1c 100644 --- a/libs/common/src/auth/login-strategies/user-api-login.strategy.ts +++ b/libs/common/src/auth/login-strategies/user-api-login.strategy.ts @@ -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.getAndSetMasterKey(keyConnectorUrl); + } + } + + protected override async setUserKey(response: IdentityTokenResponse): Promise { + await this.cryptoService.setUserSymKeyMasterKey(response.key); + + if (response.apiUseKeyConnector) { + const masterKey = await this.cryptoService.getMasterKey(); + if (masterKey) { + const userKey = await this.cryptoService.decryptUserSymKeyWithMasterKey(masterKey); + await this.cryptoService.setUserKey(userKey); + } + } + } + + protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + 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); diff --git a/libs/common/src/auth/models/domain/log-in-credentials.ts b/libs/common/src/auth/models/domain/log-in-credentials.ts index 5312b13751..ccf89359f6 100644 --- a/libs/common/src/auth/models/domain/log-in-credentials.ts +++ b/libs/common/src/auth/models/domain/log-in-credentials.ts @@ -1,4 +1,4 @@ -import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { MasterKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { AuthenticationType } from "../../enums/authentication-type"; import { TokenTwoFactorRequest } from "../request/identity-token/token-two-factor.request"; @@ -38,7 +38,7 @@ export class PasswordlessLogInCredentials { public email: string, public accessCode: string, public authRequestId: string, - public decKey: SymmetricCryptoKey, + public decKey: MasterKey, public localPasswordHash: string, public twoFactor?: TokenTwoFactorRequest ) {} diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 55dd67e071..cb976b8a24 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -16,8 +16,8 @@ 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 { PasswordStrengthServiceAbstraction } from "../../tools/password-strength"; +import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; import { KeyConnectorService } from "../abstractions/key-connector.service"; import { TokenService } from "../abstractions/token.service"; @@ -262,7 +262,7 @@ export class AuthService implements AuthServiceAbstraction { return AuthenticationStatus.Unlocked; } - async makePreloginKey(masterPassword: string, email: string): Promise { + async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); let kdf: KdfType = null; let kdfConfig: KdfConfig = null; diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index 17a891c40a..00f40e3c57 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -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"; @@ -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 getAndSetMasterKey(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.getUserKeyFromKeyConnector(url); + const keyArr = Utils.fromB64ToArray(masterKeyResponse.key); + const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; + await this.cryptoService.setMasterKey(masterKey); } catch (e) { this.handleKeyConnectorError(e); }