From 71f8ef601fb0a426264c1fdfd19edb4de2c648fa Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 25 Oct 2021 18:21:40 +0200 Subject: [PATCH] Add support for crypto agent (#520) --- angular/src/components/sso.component.ts | 5 +- common/src/abstractions/api.service.ts | 7 +++ common/src/abstractions/auth.service.ts | 2 +- common/src/models/api/ssoConfigApi.ts | 6 ++ .../account/setCryptoAgentKeyRequest.ts | 19 ++++++ .../request/cryptoAgentUserKeyRequest.ts | 7 +++ .../response/cryptoAgentUserKeyResponse.ts | 10 +++ .../models/response/identityTokenResponse.ts | 2 + common/src/services/api.service.ts | 50 +++++++++++++++ common/src/services/auth.service.ts | 63 +++++++++++++++---- 10 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 common/src/models/request/account/setCryptoAgentKeyRequest.ts create mode 100644 common/src/models/request/cryptoAgentUserKeyRequest.ts create mode 100644 common/src/models/response/cryptoAgentUserKeyResponse.ts diff --git a/angular/src/components/sso.component.ts b/angular/src/components/sso.component.ts index 3a5272a43b5..3e950bec758 100644 --- a/angular/src/components/sso.component.ts +++ b/angular/src/components/sso.component.ts @@ -140,7 +140,7 @@ export class SsoComponent { private async logIn(code: string, codeVerifier: string, orgIdFromState: string) { this.loggingIn = true; try { - this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri); + this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri, orgIdFromState); const response = await this.formPromise; if (response.twoFactor) { if (this.onSuccessfulLoginTwoFactorNavigate != null) { @@ -183,6 +183,9 @@ export class SsoComponent { } } catch (e) { this.logService.error(e); + if (e.message === 'Unable to reach crypto agent') { + this.platformUtilsService.showToast('error', null, this.i18nService.t('ssoCryptoAgentUnavailable')); + } } this.loggingIn = false; } diff --git a/common/src/abstractions/api.service.ts b/common/src/abstractions/api.service.ts index 98da82cfd81..8997c46f00f 100644 --- a/common/src/abstractions/api.service.ts +++ b/common/src/abstractions/api.service.ts @@ -1,4 +1,5 @@ import { PolicyType } from '../enums/policyType'; +import { SetCryptoAgentKeyRequest } from '../models/request/account/setCryptoAgentKeyRequest'; import { AttachmentRequest } from '../models/request/attachmentRequest'; @@ -12,6 +13,7 @@ import { CipherCreateRequest } from '../models/request/cipherCreateRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { CollectionRequest } from '../models/request/collectionRequest'; +import { CryptoAgentUserKeyRequest } from '../models/request/cryptoAgentUserKeyRequest'; import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; @@ -98,6 +100,7 @@ import { CollectionGroupDetailsResponse, CollectionResponse, } from '../models/response/collectionResponse'; +import { CryptoAgentUserKeyResponse } from '../models/response/cryptoAgentUserKeyResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { EmergencyAccessGranteeDetailsResponse, @@ -172,6 +175,7 @@ export abstract class ApiService { postEmail: (request: EmailRequest) => Promise; postPassword: (request: PasswordRequest) => Promise; setPassword: (request: SetPasswordRequest) => Promise; + postSetCryptoAgentKey: (request: SetCryptoAgentKeyRequest) => Promise; postSecurityStamp: (request: PasswordVerificationRequest) => Promise; deleteAccount: (request: PasswordVerificationRequest) => Promise; getAccountRevisionDate: () => Promise; @@ -444,4 +448,7 @@ export abstract class ApiService { nativeFetch: (request: Request) => Promise; preValidateSso: (identifier: string) => Promise; + + getUserKeyFromCryptoAgent: (cryptoAgentUrl: string) => Promise; + postUserKeyToCryptoAgent: (cryptoAgentUrl: string, request: CryptoAgentUserKeyRequest) => Promise; } diff --git a/common/src/abstractions/auth.service.ts b/common/src/abstractions/auth.service.ts index 7307bbdf675..58f72fc7495 100644 --- a/common/src/abstractions/auth.service.ts +++ b/common/src/abstractions/auth.service.ts @@ -15,7 +15,7 @@ export abstract class AuthService { selectedTwoFactorProviderType: TwoFactorProviderType; logIn: (email: string, masterPassword: string, captchaToken?: string) => Promise; - logInSso: (code: string, codeVerifier: string, redirectUrl: string) => Promise; + logInSso: (code: string, codeVerifier: string, redirectUrl: string, orgId: string) => Promise; logInApiKey: (clientId: string, clientSecret: string) => Promise; logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; diff --git a/common/src/models/api/ssoConfigApi.ts b/common/src/models/api/ssoConfigApi.ts index 9c5880aa175..ce7e5b833aa 100644 --- a/common/src/models/api/ssoConfigApi.ts +++ b/common/src/models/api/ssoConfigApi.ts @@ -37,6 +37,9 @@ enum Saml2SigningBehavior { export class SsoConfigApi extends BaseResponse { configType: SsoType; + useCryptoAgent: boolean; + cryptoAgentUrl: string; + // OpenId authority: string; clientId: string; @@ -78,6 +81,9 @@ export class SsoConfigApi extends BaseResponse { this.configType = this.getResponseProperty('ConfigType'); + this.useCryptoAgent = this.getResponseProperty('UseCryptoAgent'); + this.cryptoAgentUrl = this.getResponseProperty('CryptoAgentUrl'); + this.authority = this.getResponseProperty('Authority'); this.clientId = this.getResponseProperty('ClientId'); this.clientSecret = this.getResponseProperty('ClientSecret'); diff --git a/common/src/models/request/account/setCryptoAgentKeyRequest.ts b/common/src/models/request/account/setCryptoAgentKeyRequest.ts new file mode 100644 index 00000000000..7f76af3d219 --- /dev/null +++ b/common/src/models/request/account/setCryptoAgentKeyRequest.ts @@ -0,0 +1,19 @@ +import { KeysRequest } from '../keysRequest'; + +import { KdfType } from '../../../enums/kdfType'; + +export class SetCryptoAgentKeyRequest { + key: string; + keys: KeysRequest; + kdf: KdfType; + kdfIterations: number; + orgIdentifier: string; + + constructor(key: string, kdf: KdfType, kdfIterations: number, orgIdentifier: string, keys: KeysRequest) { + this.key = key; + this.kdf = kdf; + this.kdfIterations = kdfIterations; + this.orgIdentifier = orgIdentifier; + this.keys = keys; + } +} diff --git a/common/src/models/request/cryptoAgentUserKeyRequest.ts b/common/src/models/request/cryptoAgentUserKeyRequest.ts new file mode 100644 index 00000000000..87363a153ab --- /dev/null +++ b/common/src/models/request/cryptoAgentUserKeyRequest.ts @@ -0,0 +1,7 @@ +export class CryptoAgentUserKeyRequest { + key: string; + + constructor(key: string) { + this.key = key; + } +} diff --git a/common/src/models/response/cryptoAgentUserKeyResponse.ts b/common/src/models/response/cryptoAgentUserKeyResponse.ts new file mode 100644 index 00000000000..0397388eaf9 --- /dev/null +++ b/common/src/models/response/cryptoAgentUserKeyResponse.ts @@ -0,0 +1,10 @@ +import { BaseResponse } from './baseResponse'; + +export class CryptoAgentUserKeyResponse extends BaseResponse { + key: string; + + constructor(response: any) { + super(response); + this.key = this.getResponseProperty('Key'); + } +} diff --git a/common/src/models/response/identityTokenResponse.ts b/common/src/models/response/identityTokenResponse.ts index 2a6fd9a06ef..aa2fe8fdd2d 100644 --- a/common/src/models/response/identityTokenResponse.ts +++ b/common/src/models/response/identityTokenResponse.ts @@ -15,6 +15,7 @@ export class IdentityTokenResponse extends BaseResponse { kdf: KdfType; kdfIterations: number; forcePasswordReset: boolean; + cryptoAgentUrl: string; constructor(response: any) { super(response); @@ -30,5 +31,6 @@ export class IdentityTokenResponse extends BaseResponse { this.kdf = this.getResponseProperty('Kdf'); this.kdfIterations = this.getResponseProperty('KdfIterations'); this.forcePasswordReset = this.getResponseProperty('ForcePasswordReset'); + this.cryptoAgentUrl = this.getResponseProperty('CryptoAgentUrl'); } } diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index 4417d2758db..81d19a32b37 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -166,6 +166,9 @@ import { ChallengeResponse } from '../models/response/twoFactorWebAuthnResponse' import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse'; import { UserKeyResponse } from '../models/response/userKeyResponse'; +import { SetCryptoAgentKeyRequest } from '../models/request/account/setCryptoAgentKeyRequest'; +import { CryptoAgentUserKeyRequest } from '../models/request/cryptoAgentUserKeyRequest'; +import { CryptoAgentUserKeyResponse } from '../models/response/cryptoAgentUserKeyResponse'; import { SendAccessView } from '../models/view/sendAccessView'; export class ApiService implements ApiServiceAbstraction { @@ -289,6 +292,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/set-password', request, true, false); } + postSetCryptoAgentKey(request: SetCryptoAgentKeyRequest): Promise { + return this.send('POST', '/accounts/set-crypto-agent-key', request, true, false); + } + postSecurityStamp(request: PasswordVerificationRequest): Promise { return this.send('POST', '/accounts/security-stamp', request, true, false); } @@ -1429,6 +1436,49 @@ export class ApiService implements ApiServiceAbstraction { return r as string; } + // Crypto Agent + + async getUserKeyFromCryptoAgent(cryptoAgentUrl: string): Promise { + const authHeader = await this.getActiveBearerToken(); + + const response = await this.fetch(new Request(cryptoAgentUrl + '/user-keys', { + cache: 'no-store', + method: 'GET', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': 'Bearer ' + authHeader, + }), + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false, true); + return Promise.reject(error); + } + + return new CryptoAgentUserKeyResponse(await response.json()); + } + + async postUserKeyToCryptoAgent(cryptoAgentUrl: string, request: CryptoAgentUserKeyRequest): Promise { + const authHeader = await this.getActiveBearerToken(); + + const response = await this.fetch(new Request(cryptoAgentUrl + '/user-keys', { + cache: 'no-store', + method: 'POST', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': 'Bearer ' + authHeader, + 'Content-Type': 'application/json; charset=utf-8', + }), + body: JSON.stringify(request), + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false, true); + return Promise.reject(error); + } + } + + // Helpers async getActiveBearerToken(): Promise { diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index 7bd0b608228..f92e2df8e18 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -5,6 +5,8 @@ import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; import { AuthResult } from '../models/domain/authResult'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { SetCryptoAgentKeyRequest } from '../models/request/account/setCryptoAgentKeyRequest'; +import { CryptoAgentUserKeyRequest } from '../models/request/cryptoAgentUserKeyRequest'; import { DeviceRequest } from '../models/request/deviceRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; @@ -17,6 +19,7 @@ import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { AuthService as AuthServiceAbstraction } from '../abstractions/auth.service'; import { CryptoService } from '../abstractions/crypto.service'; +import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { I18nService } from '../abstractions/i18n.service'; import { LogService } from '../abstractions/log.service'; import { MessagingService } from '../abstractions/messaging.service'; @@ -25,6 +28,8 @@ import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; +import { Utils } from '../misc/utils'; + export const TwoFactorProviders = { [TwoFactorProviderType.Authenticator]: { type: TwoFactorProviderType.Authenticator, @@ -96,7 +101,7 @@ export class AuthService implements AuthServiceAbstraction { protected appIdService: AppIdService, private i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, private vaultTimeoutService: VaultTimeoutService, private logService: LogService, - private setCryptoKeys = true) { + private cryptoFunctionService: CryptoFunctionService, private setCryptoKeys = true) { } init() { @@ -128,26 +133,26 @@ export class AuthService implements AuthServiceAbstraction { const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key, HashPurpose.LocalAuthorization); return await this.logInHelper(email, hashedPassword, localHashedPassword, null, null, null, null, null, - key, null, null, null, captchaToken); + key, null, null, null, captchaToken, null); } - async logInSso(code: string, codeVerifier: string, redirectUrl: string): Promise { + async logInSso(code: string, codeVerifier: string, redirectUrl: string, orgId: string): Promise { this.selectedTwoFactorProviderType = null; return await this.logInHelper(null, null, null, code, codeVerifier, redirectUrl, null, null, - null, null, null, null); + null, null, null, null, null, orgId); } async logInApiKey(clientId: string, clientSecret: string): Promise { this.selectedTwoFactorProviderType = null; return await this.logInHelper(null, null, null, null, null, null, clientId, clientSecret, - null, null, null, null); + null, null, null, null, null, null); } async logInTwoFactor(twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { return await this.logInHelper(this.email, this.masterPasswordHash, this.localMasterPasswordHash, this.code, this.codeVerifier, this.ssoRedirectUrl, this.clientId, this.clientSecret, this.key, twoFactorProvider, - twoFactorToken, remember, this.captchaToken); + twoFactorToken, remember, this.captchaToken, null); } async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, @@ -158,21 +163,21 @@ export class AuthService implements AuthServiceAbstraction { const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key, HashPurpose.LocalAuthorization); return await this.logInHelper(email, hashedPassword, localHashedPassword, null, null, null, null, null, key, - twoFactorProvider, twoFactorToken, remember, captchaToken); + twoFactorProvider, twoFactorToken, remember, captchaToken, null); } async logInSsoComplete(code: string, codeVerifier: string, redirectUrl: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { this.selectedTwoFactorProviderType = null; return await this.logInHelper(null, null, null, code, codeVerifier, redirectUrl, null, - null, null, twoFactorProvider, twoFactorToken, remember); + null, null, twoFactorProvider, twoFactorToken, remember, null, null); } async logInApiKeyComplete(clientId: string, clientSecret: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { this.selectedTwoFactorProviderType = null; return await this.logInHelper(null, null, null, null, null, null, clientId, clientSecret, null, - twoFactorProvider, twoFactorToken, remember); + twoFactorProvider, twoFactorToken, remember, null, null); } logOut(callback: Function) { @@ -273,7 +278,8 @@ export class AuthService implements AuthServiceAbstraction { private async logInHelper(email: string, hashedPassword: string, localHashedPassword: string, code: string, codeVerifier: string, redirectUrl: string, clientId: string, clientSecret: string, key: SymmetricCryptoKey, - twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean, captchaToken?: string): Promise { + twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean, captchaToken?: string, + orgId?: string): Promise { const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); const appId = await this.appIdService.getAppId(); const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); @@ -358,6 +364,19 @@ export class AuthService implements AuthServiceAbstraction { // Skip this step during SSO new user flow. No key is returned from server. if (code == null || tokenResponse.key != null) { + + if (tokenResponse.cryptoAgentUrl != null) { + try { + const userKeyResponse = await this.apiService.getUserKeyFromCryptoAgent(tokenResponse.cryptoAgentUrl); + const keyArr = Utils.fromB64ToArray(userKeyResponse.key); + const k = new SymmetricCryptoKey(keyArr); + await this.cryptoService.setKey(k); + } catch (e) { + this.logService.error(e); + throw new Error('Unable to reach crypto agent'); + } + } + await this.cryptoService.setEncKey(tokenResponse.key); // User doesn't have a key pair yet (old account), let's generate one for them @@ -367,12 +386,34 @@ export class AuthService implements AuthServiceAbstraction { await this.apiService.postAccountKeys(new KeysRequest(keyPair[0], keyPair[1].encryptedString)); tokenResponse.privateKey = keyPair[1].encryptedString; } catch (e) { - // tslint:disable-next-line this.logService.error(e); } } await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); + } else if (tokenResponse.cryptoAgentUrl != null) { + const password = await this.cryptoFunctionService.randomBytes(64); + + const k = await this.cryptoService.makeKey(Utils.fromBufferToB64(password), this.tokenService.getEmail(), tokenResponse.kdf, tokenResponse.kdfIterations); + const cryptoAgentRequest = new CryptoAgentUserKeyRequest(k.encKeyB64); + await this.cryptoService.setKey(k); + + const encKey = await this.cryptoService.makeEncKey(k); + await this.cryptoService.setEncKey(encKey[1].encryptedString); + + const [pubKey, privKey] = await this.cryptoService.makeKeyPair(); + + try { + await this.apiService.postUserKeyToCryptoAgent(tokenResponse.cryptoAgentUrl, cryptoAgentRequest); + } catch (e) { + throw new Error('Unable to reach crypto agent'); + } + + const keys = new KeysRequest(pubKey, privKey.encryptedString); + const setPasswordRequest = new SetCryptoAgentKeyRequest( + encKey[1].encryptedString, tokenResponse.kdf, tokenResponse.kdfIterations, orgId, keys + ); + await this.apiService.postSetCryptoAgentKey(setPasswordRequest); } }