From 8f177e2d3a879b854db5c6e6d7d386b24d637a66 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 9 Nov 2021 17:01:22 +0100 Subject: [PATCH] Add support for requesting and using otp for verifying some requests (#527) Co-authored-by: Thomas Rittson --- angular/src/components/add-edit.component.ts | 7 +- angular/src/components/export.component.ts | 65 ++++++----- angular/src/components/lock.component.ts | 13 ++- .../components/remove-password.component.ts | 77 +++++++++++++ angular/src/components/sso.component.ts | 8 +- .../src/components/two-factor.component.ts | 4 +- .../verify-master-password.component.html | 18 +++ .../verify-master-password.component.ts | 81 +++++++++++++ angular/src/services/auth-guard.service.ts | 8 +- .../src/services/passwordReprompt.service.ts | 11 +- common/src/abstractions/api.service.ts | 52 +++++---- .../src/abstractions/environment.service.ts | 2 + .../src/abstractions/keyConnector.service.ts | 14 +++ .../abstractions/passwordReprompt.service.ts | 1 + common/src/abstractions/token.service.ts | 1 + common/src/abstractions/user.service.ts | 1 + .../abstractions/userVerification.service.ts | 9 ++ common/src/enums/eventType.ts | 5 + common/src/enums/verificationType.ts | 4 + common/src/models/api/ssoConfigApi.ts | 8 +- common/src/models/data/organizationData.ts | 4 + common/src/models/domain/organization.ts | 4 + ...equest.ts => setKeyConnectorKeyRequest.ts} | 2 +- .../request/account/verifyOTPRequest.ts | 7 ++ .../src/models/request/emailTokenRequest.ts | 4 +- ...quest.ts => keyConnectorUserKeyRequest.ts} | 2 +- common/src/models/request/passwordRequest.ts | 4 +- .../request/passwordVerificationRequest.ts | 3 - .../request/secretVerificationRequest.ts | 4 + .../models/request/twoFactorEmailRequest.ts | 10 +- .../request/twoFactorProviderRequest.ts | 4 +- .../request/twoFactorRecoveryRequest.ts | 4 +- .../updateTwoFactorAuthenticatorRequest.ts | 4 +- .../request/updateTwoFactorDuoRequest.ts | 4 +- .../request/updateTwoFactorEmailRequest.ts | 4 +- .../updateTwoFactorWebAuthnDeleteRequest.ts | 4 +- .../request/updateTwoFactorWebAuthnRequest.ts | 4 +- .../request/updateTwoFactorYubioOtpRequest.ts | 4 +- .../models/response/identityTokenResponse.ts | 4 +- ...onse.ts => keyConnectorUserKeyResponse.ts} | 2 +- .../response/profileOrganizationResponse.ts | 4 + .../profileProviderOrganizationResponse.ts | 66 +---------- common/src/models/response/profileResponse.ts | 2 + common/src/services/api.service.ts | 71 +++++++----- common/src/services/auth.service.ts | 35 +++--- common/src/services/environment.service.ts | 11 ++ common/src/services/keyConnector.service.ts | 106 ++++++++++++++++++ common/src/services/sync.service.ts | 11 ++ common/src/services/token.service.ts | 9 ++ common/src/services/user.service.ts | 1 + .../src/services/userVerification.service.ts | 70 ++++++++++++ common/src/services/vaultTimeout.service.ts | 11 ++ common/src/types/verification.ts | 6 + node/src/cli/commands/login.command.ts | 95 ++++++++++++++-- 54 files changed, 746 insertions(+), 223 deletions(-) create mode 100644 angular/src/components/remove-password.component.ts create mode 100644 angular/src/components/verify-master-password.component.html create mode 100644 angular/src/components/verify-master-password.component.ts create mode 100644 common/src/abstractions/keyConnector.service.ts create mode 100644 common/src/abstractions/userVerification.service.ts create mode 100644 common/src/enums/verificationType.ts rename common/src/models/request/account/{setCryptoAgentKeyRequest.ts => setKeyConnectorKeyRequest.ts} (92%) create mode 100644 common/src/models/request/account/verifyOTPRequest.ts rename common/src/models/request/{cryptoAgentUserKeyRequest.ts => keyConnectorUserKeyRequest.ts} (65%) delete mode 100644 common/src/models/request/passwordVerificationRequest.ts create mode 100644 common/src/models/request/secretVerificationRequest.ts rename common/src/models/response/{cryptoAgentUserKeyResponse.ts => keyConnectorUserKeyResponse.ts} (74%) create mode 100644 common/src/services/keyConnector.service.ts create mode 100644 common/src/services/userVerification.service.ts create mode 100644 common/src/types/verification.ts diff --git a/angular/src/components/add-edit.component.ts b/angular/src/components/add-edit.component.ts index 3c6d0bc3b67..0bd4001b1db 100644 --- a/angular/src/components/add-edit.component.ts +++ b/angular/src/components/add-edit.component.ts @@ -22,6 +22,7 @@ import { FolderService } from 'jslib-common/abstractions/folder.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; +import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { StateService } from 'jslib-common/abstractions/state.service'; @@ -80,6 +81,7 @@ export class AddEditComponent implements OnInit { currentDate = new Date(); allowPersonal = true; reprompt: boolean = false; + canUseReprompt: boolean = true; protected writeableCollections: CollectionView[]; private previousCipherId: string; @@ -89,7 +91,8 @@ export class AddEditComponent implements OnInit { protected auditService: AuditService, protected stateService: StateService, protected userService: UserService, protected collectionService: CollectionService, protected messagingService: MessagingService, protected eventService: EventService, - protected policyService: PolicyService, private logService: LogService) { + protected policyService: PolicyService, protected passwordRepromptService: PasswordRepromptService, + private logService: LogService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, { name: i18nService.t('typeCard'), value: CipherType.Card }, @@ -169,6 +172,8 @@ export class AddEditComponent implements OnInit { } this.writeableCollections = await this.loadCollections(); + + this.canUseReprompt = await this.passwordRepromptService.enabled(); } async load() { diff --git a/angular/src/components/export.component.ts b/angular/src/components/export.component.ts index 3ea63dee54a..8b9372c340f 100644 --- a/angular/src/components/export.component.ts +++ b/angular/src/components/export.component.ts @@ -4,6 +4,7 @@ import { OnInit, Output, } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { EventService } from 'jslib-common/abstractions/event.service'; @@ -12,6 +13,7 @@ import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; +import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service'; import { EventType } from 'jslib-common/enums/eventType'; import { PolicyType } from 'jslib-common/enums/policyType'; @@ -21,15 +23,24 @@ export class ExportComponent implements OnInit { @Output() onSaved = new EventEmitter(); formPromise: Promise; - masterPassword: string; - format: 'json' | 'encrypted_json' | 'csv' = 'json'; - showPassword = false; disabledByPolicy: boolean = false; + exportForm = this.fb.group({ + format: ['json'], + secret: [''], + }); + + formatOptions = [ + { name: '.json', value: 'json' }, + { name: '.csv', value: 'csv' }, + { name: '.json (Encrypted)', value: 'encrypted_json' }, + ]; + constructor(protected cryptoService: CryptoService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService, protected eventService: EventService, private policyService: PolicyService, protected win: Window, - private logService: LogService) { } + private logService: LogService, private userVerificationService: UserVerificationService, + private fb: FormBuilder) { } async ngOnInit() { await this.checkExportDisabled(); @@ -37,6 +48,9 @@ export class ExportComponent implements OnInit { async checkExportDisabled() { this.disabledByPolicy = await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport); + if (this.disabledByPolicy) { + this.exportForm.disable(); + } } get encryptedFormat() { @@ -49,31 +63,25 @@ export class ExportComponent implements OnInit { return; } - if (this.masterPassword == null || this.masterPassword === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('invalidMasterPassword')); - return; - } - const acceptedWarning = await this.warningDialog(); if (!acceptedWarning) { return; } - const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, null); - if (passwordValid) { - try { - this.formPromise = this.getExportData(); - const data = await this.formPromise; - this.downloadFile(data); - this.saved(); - await this.collectEvent(); - } catch (e) { - this.logService.error(e); - } - } else { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('invalidMasterPassword')); + const secret = this.exportForm.get('secret').value; + if (!await this.userVerificationService.verifyUser(secret)) { + return; + } + + try { + this.formPromise = this.getExportData(); + const data = await this.formPromise; + this.downloadFile(data); + this.saved(); + await this.collectEvent(); + this.exportForm.get('secret').setValue(''); + } catch (e) { + this.logService.error(e); } } @@ -93,11 +101,6 @@ export class ExportComponent implements OnInit { } } - togglePassword() { - this.showPassword = !this.showPassword; - document.getElementById('masterPassword').focus(); - } - protected saved() { this.onSaved.emit(); } @@ -123,6 +126,10 @@ export class ExportComponent implements OnInit { await this.eventService.collect(EventType.User_ClientExportedVault); } + get format() { + return this.exportForm.get('format').value; + } + private downloadFile(csv: string): void { const fileName = this.getFileName(); this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName); diff --git a/angular/src/components/lock.component.ts b/angular/src/components/lock.component.ts index 329dd0c6a30..34f34f1134f 100644 --- a/angular/src/components/lock.component.ts +++ b/angular/src/components/lock.component.ts @@ -5,6 +5,7 @@ import { ApiService } from 'jslib-common/abstractions/api.service'; import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { LogService } from 'jslib-common/abstractions/log.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; @@ -18,7 +19,7 @@ import { ConstantsService } from 'jslib-common/services/constants.service'; import { EncString } from 'jslib-common/models/domain/encString'; import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; -import { PasswordVerificationRequest } from 'jslib-common/models/request/passwordVerificationRequest'; +import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest'; import { Utils } from 'jslib-common/misc/utils'; @@ -48,7 +49,8 @@ export class LockComponent implements OnInit { protected userService: UserService, protected cryptoService: CryptoService, protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService, protected environmentService: EnvironmentService, protected stateService: StateService, - protected apiService: ApiService, private logService: LogService) { } + protected apiService: ApiService, private logService: LogService, + private keyConnectorService: KeyConnectorService) { } async ngOnInit() { this.pinSet = await this.vaultTimeoutService.isPinLockSet(); @@ -59,6 +61,11 @@ export class LockComponent implements OnInit { this.biometricText = await this.storageService.get(ConstantsService.biometricText); this.email = await this.userService.getEmail(); + // Users with key connector and without biometric or pin has no MP to unlock using + if (await this.keyConnectorService.getUsesKeyConnector() && !(this.biometricLock || this.pinLock)) { + await this.vaultTimeoutService.logOut(); + } + const webVaultUrl = this.environmentService.getWebVaultUrl(); const vaultUrl = webVaultUrl === 'https://vault.bitwarden.com' ? 'https://bitwarden.com' : webVaultUrl; this.webVaultHostname = Utils.getHostname(vaultUrl); @@ -119,7 +126,7 @@ export class LockComponent implements OnInit { if (storedKeyHash != null) { passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, key); } else { - const request = new PasswordVerificationRequest(); + const request = new SecretVerificationRequest(); const serverKeyHash = await this.cryptoService.hashPassword(this.masterPassword, key, HashPurpose.ServerAuthorization); request.masterPasswordHash = serverKeyHash; diff --git a/angular/src/components/remove-password.component.ts b/angular/src/components/remove-password.component.ts new file mode 100644 index 00000000000..91c35312b03 --- /dev/null +++ b/angular/src/components/remove-password.component.ts @@ -0,0 +1,77 @@ +import { + Directive, + OnInit, +} from '@angular/core'; +import { Router } from '@angular/router'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { UserService } from 'jslib-common/abstractions/user.service'; + +import { ConstantsService } from 'jslib-common/services/constants.service'; + +import { Organization } from 'jslib-common/models/domain/organization'; + +@Directive() +export class RemovePasswordComponent implements OnInit { + + actionPromise: Promise; + continuing: boolean = false; + leaving: boolean = false; + + loading: boolean = true; + organization: Organization; + email: string; + + constructor(private router: Router, private userService: UserService, + private apiService: ApiService, private syncService: SyncService, + private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private keyConnectorService: KeyConnectorService, private storageService: StorageService) { } + + async ngOnInit() { + this.organization = await this.keyConnectorService.getManagingOrganization(); + this.email = await this.userService.getEmail(); + await this.syncService.fullSync(false); + this.loading = false; + } + + async convert() { + this.continuing = true; + this.actionPromise = this.keyConnectorService.migrateUser(); + + try { + await this.actionPromise; + this.platformUtilsService.showToast('success', null, this.i18nService.t('removedMasterPassword')); + await this.keyConnectorService.removeConvertAccountRequired(); + this.router.navigate(['']); + } catch (e) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), e.message); + } + } + + async leave() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('leaveOrganizationConfirmation'), this.organization.name, + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.leaving = true; + this.actionPromise = this.apiService.postLeaveOrganization(this.organization.id).then(() => { + return this.syncService.fullSync(true); + }); + await this.actionPromise; + this.platformUtilsService.showToast('success', null, this.i18nService.t('leftOrganization')); + await this.keyConnectorService.removeConvertAccountRequired(); + this.router.navigate(['']); + } catch (e) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), e); + } + } +} diff --git a/angular/src/components/sso.component.ts b/angular/src/components/sso.component.ts index 3e950bec758..1ab8e2f4047 100644 --- a/angular/src/components/sso.component.ts +++ b/angular/src/components/sso.component.ts @@ -60,7 +60,7 @@ export class SsoComponent { await this.storageService.remove(ConstantsService.ssoCodeVerifierKey); await this.storageService.remove(ConstantsService.ssoStateKey); if (qParams.code != null && codeVerifier != null && state != null && this.checkState(state, qParams.state)) { - await this.logIn(qParams.code, codeVerifier, this.getOrgIdentiferFromState(qParams.state)); + await this.logIn(qParams.code, codeVerifier, this.getOrgIdentifierFromState(qParams.state)); } } else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null && qParams.codeChallenge != null) { @@ -183,14 +183,14 @@ 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')); + if (e.message === 'Unable to reach key connector') { + this.platformUtilsService.showToast('error', null, this.i18nService.t('ssoKeyConnectorUnavailable')); } } this.loggingIn = false; } - private getOrgIdentiferFromState(state: string): string { + private getOrgIdentifierFromState(state: string): string { if (state === null || state === undefined) { return null; } diff --git a/angular/src/components/two-factor.component.ts b/angular/src/components/two-factor.component.ts index 18deda6e45d..5ad45a25252 100644 --- a/angular/src/components/two-factor.component.ts +++ b/angular/src/components/two-factor.component.ts @@ -211,7 +211,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } try { - const request = new TwoFactorEmailRequest(this.authService.email, this.authService.masterPasswordHash); + const request = new TwoFactorEmailRequest(); + request.email = this.authService.email; + request.masterPasswordHash = this.authService.masterPasswordHash; this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; if (doToast) { diff --git a/angular/src/components/verify-master-password.component.html b/angular/src/components/verify-master-password.component.html new file mode 100644 index 00000000000..ff34926a304 --- /dev/null +++ b/angular/src/components/verify-master-password.component.html @@ -0,0 +1,18 @@ + + + + + +
+ +
+ +
+ + +
+
diff --git a/angular/src/components/verify-master-password.component.ts b/angular/src/components/verify-master-password.component.ts new file mode 100644 index 00000000000..01c511c916c --- /dev/null +++ b/angular/src/components/verify-master-password.component.ts @@ -0,0 +1,81 @@ +import { + Component, + OnInit, +} from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + NG_VALUE_ACCESSOR, +} from '@angular/forms'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; + +import { VerificationType } from 'jslib-common/enums/verificationType'; + +import { Verification } from 'jslib-common/types/verification'; + +@Component({ + selector: 'app-verify-master-password', + templateUrl: 'verify-master-password.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: VerifyMasterPasswordComponent, + }, + ], +}) +export class VerifyMasterPasswordComponent implements ControlValueAccessor, OnInit { + usesKeyConnector: boolean = false; + disableRequestOTP: boolean = false; + + secret = new FormControl(''); + + private onChange: (value: Verification) => void; + + constructor(private keyConnectorService: KeyConnectorService, private apiService: ApiService) { } + + async ngOnInit() { + this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); + + this.secret.valueChanges.subscribe(secret => { + if (this.onChange == null) { + return; + } + + this.onChange({ + type: this.usesKeyConnector ? VerificationType.OTP : VerificationType.MasterPassword, + secret: secret, + }); + }); + } + + async requestOTP() { + if (this.usesKeyConnector) { + this.disableRequestOTP = true; + await this.apiService.postAccountRequestOTP(); + } + } + + writeValue(obj: any): void { + this.secret.setValue(obj); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + // Not implemented + } + + setDisabledState?(isDisabled: boolean): void { + this.disableRequestOTP = isDisabled; + if (isDisabled) { + this.secret.disable(); + } else { + this.secret.enable(); + } + } +} diff --git a/angular/src/services/auth-guard.service.ts b/angular/src/services/auth-guard.service.ts index f80f8acaaf2..5656157756b 100644 --- a/angular/src/services/auth-guard.service.ts +++ b/angular/src/services/auth-guard.service.ts @@ -6,6 +6,7 @@ import { RouterStateSnapshot, } from '@angular/router'; +import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { UserService } from 'jslib-common/abstractions/user.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; @@ -13,7 +14,7 @@ import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.serv @Injectable() export class AuthGuardService implements CanActivate { constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService, - private router: Router, private messagingService: MessagingService) { } + private router: Router, private messagingService: MessagingService, private keyConnectorService: KeyConnectorService) { } async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { const isAuthed = await this.userService.isAuthenticated(); @@ -31,6 +32,11 @@ export class AuthGuardService implements CanActivate { return false; } + if (!routerState.url.includes('remove-password') && await this.keyConnectorService.getConvertAccountRequired()) { + this.router.navigate(['/remove-password']); + return false; + } + return true; } } diff --git a/angular/src/services/passwordReprompt.service.ts b/angular/src/services/passwordReprompt.service.ts index 552e558b467..03e045310e9 100644 --- a/angular/src/services/passwordReprompt.service.ts +++ b/angular/src/services/passwordReprompt.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; +import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib-common/abstractions/passwordReprompt.service'; import { PasswordRepromptComponent } from '../components/password-reprompt.component'; @@ -9,13 +10,17 @@ import { ModalService } from './modal.service'; export class PasswordRepromptService implements PasswordRepromptServiceAbstraction { protected component = PasswordRepromptComponent; - constructor(private modalService: ModalService) { } + constructor(private modalService: ModalService, private keyConnectorService: KeyConnectorService) { } protectedFields() { return ['TOTP', 'Password', 'H_Field', 'Card Number', 'Security Code']; } async showPasswordPrompt() { + if (!await this.enabled()) { + return true; + } + const ref = this.modalService.open(this.component, {allowMultipleModals: true}); if (ref == null) { @@ -25,4 +30,8 @@ export class PasswordRepromptService implements PasswordRepromptServiceAbstracti const result = await ref.onClosedPromise(); return result === true; } + + async enabled() { + return !await this.keyConnectorService.getUsesKeyConnector(); + } } diff --git a/common/src/abstractions/api.service.ts b/common/src/abstractions/api.service.ts index 8997c46f00f..e146a4f5998 100644 --- a/common/src/abstractions/api.service.ts +++ b/common/src/abstractions/api.service.ts @@ -1,5 +1,6 @@ import { PolicyType } from '../enums/policyType'; -import { SetCryptoAgentKeyRequest } from '../models/request/account/setCryptoAgentKeyRequest'; +import { SetKeyConnectorKeyRequest } from '../models/request/account/setKeyConnectorKeyRequest'; +import { VerifyOTPRequest } from '../models/request/account/verifyOTPRequest'; import { AttachmentRequest } from '../models/request/attachmentRequest'; @@ -13,7 +14,6 @@ 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'; @@ -30,6 +30,7 @@ import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { KdfRequest } from '../models/request/kdfRequest'; +import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationSsoRequest } from '../models/request/organization/organizationSsoRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; @@ -50,7 +51,6 @@ import { OrganizationUserUpdateGroupsRequest } from '../models/request/organizat import { OrganizationUserUpdateRequest } from '../models/request/organizationUserUpdateRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; -import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; import { PolicyRequest } from '../models/request/policyRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; @@ -66,6 +66,7 @@ import { ProviderUserInviteRequest } from '../models/request/provider/providerUs import { ProviderUserUpdateRequest } from '../models/request/provider/providerUserUpdateRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; +import { SecretVerificationRequest } from '../models/request/secretVerificationRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; import { SendAccessRequest } from '../models/request/sendAccessRequest'; import { SendRequest } from '../models/request/sendRequest'; @@ -100,7 +101,6 @@ import { CollectionGroupDetailsResponse, CollectionResponse, } from '../models/response/collectionResponse'; -import { CryptoAgentUserKeyResponse } from '../models/response/cryptoAgentUserKeyResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { EmergencyAccessGranteeDetailsResponse, @@ -117,6 +117,7 @@ import { import { IdentityCaptchaResponse } from '../models/response/identityCaptchaResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { KeyConnectorUserKeyResponse } from '../models/response/keyConnectorUserKeyResponse'; import { ListResponse } from '../models/response/listResponse'; import { OrganizationSsoResponse } from '../models/response/organization/organizationSsoResponse'; import { OrganizationAutoEnrollStatusResponse } from '../models/response/organizationAutoEnrollStatusResponse'; @@ -175,9 +176,9 @@ 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; + postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise; + postSecurityStamp: (request: SecretVerificationRequest) => Promise; + deleteAccount: (request: SecretVerificationRequest) => Promise; getAccountRevisionDate: () => Promise; postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; @@ -192,13 +193,16 @@ export abstract class ApiService { postAccountKeys: (request: KeysRequest) => Promise; postAccountVerifyEmail: () => Promise; postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; - postAccountVerifyPassword: (request: PasswordVerificationRequest) => Promise; + postAccountVerifyPassword: (request: SecretVerificationRequest) => Promise; postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postAccountKdf: (request: KdfRequest) => Promise; - postUserApiKey: (id: string, request: PasswordVerificationRequest) => Promise; - postUserRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise; + postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise; + postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise; putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise; + postAccountRequestOTP: () => Promise; + postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise; + postConvertToKeyConnector: () => Promise; getFolder: (id: string) => Promise; postFolder: (request: FolderRequest) => Promise; @@ -240,7 +244,7 @@ export abstract class ApiService { putShareCiphers: (request: CipherBulkShareRequest) => Promise; putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; - postPurgeCiphers: (request: PasswordVerificationRequest, organizationId?: string) => Promise; + postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise; postImportCiphers: (request: ImportCiphersRequest) => Promise; postImportOrganizationCiphers: (organizationId: string, request: ImportOrganizationCiphersRequest) => Promise; putDeleteCipher: (id: string) => Promise; @@ -329,15 +333,15 @@ export abstract class ApiService { getTwoFactorProviders: () => Promise>; getTwoFactorOrganizationProviders: (organizationId: string) => Promise>; - getTwoFactorAuthenticator: (request: PasswordVerificationRequest) => Promise; - getTwoFactorEmail: (request: PasswordVerificationRequest) => Promise; - getTwoFactorDuo: (request: PasswordVerificationRequest) => Promise; + getTwoFactorAuthenticator: (request: SecretVerificationRequest) => Promise; + getTwoFactorEmail: (request: SecretVerificationRequest) => Promise; + getTwoFactorDuo: (request: SecretVerificationRequest) => Promise; getTwoFactorOrganizationDuo: (organizationId: string, - request: PasswordVerificationRequest) => Promise; - getTwoFactorYubiKey: (request: PasswordVerificationRequest) => Promise; - getTwoFactorWebAuthn: (request: PasswordVerificationRequest) => Promise; - getTwoFactorWebAuthnChallenge: (request: PasswordVerificationRequest) => Promise; - getTwoFactorRecover: (request: PasswordVerificationRequest) => Promise; + request: SecretVerificationRequest) => Promise; + getTwoFactorYubiKey: (request: SecretVerificationRequest) => Promise; + getTwoFactorWebAuthn: (request: SecretVerificationRequest) => Promise; + getTwoFactorWebAuthnChallenge: (request: SecretVerificationRequest) => Promise; + getTwoFactorRecover: (request: SecretVerificationRequest) => Promise; putTwoFactorAuthenticator: ( request: UpdateTwoFactorAuthenticatorRequest) => Promise; putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise; @@ -384,8 +388,8 @@ export abstract class ApiService { postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise; - postOrganizationApiKey: (id: string, request: PasswordVerificationRequest) => Promise; - postOrganizationRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise; + postOrganizationApiKey: (id: string, request: SecretVerificationRequest) => Promise; + postOrganizationRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise; postOrganizationSso: (id: string, request: OrganizationSsoRequest) => Promise; postOrganizationUpgrade: (id: string, request: OrganizationUpgradeRequest) => Promise; postOrganizationUpdateSubscription: (id: string, request: OrganizationSubscriptionUpdateRequest) => Promise; @@ -395,7 +399,7 @@ export abstract class ApiService { postOrganizationVerifyBank: (id: string, request: VerifyBankRequest) => Promise; postOrganizationCancel: (id: string) => Promise; postOrganizationReinstate: (id: string) => Promise; - deleteOrganization: (id: string, request: PasswordVerificationRequest) => Promise; + deleteOrganization: (id: string, request: SecretVerificationRequest) => Promise; getPlans: () => Promise>; getTaxRates: () => Promise>; getOrganizationKeys: (id: string) => Promise; @@ -449,6 +453,6 @@ export abstract class ApiService { preValidateSso: (identifier: string) => Promise; - getUserKeyFromCryptoAgent: (cryptoAgentUrl: string) => Promise; - postUserKeyToCryptoAgent: (cryptoAgentUrl: string, request: CryptoAgentUserKeyRequest) => Promise; + getUserKeyFromKeyConnector: (keyConnectorUrl: string) => Promise; + postUserKeyToKeyConnector: (keyConnectorUrl: string, request: KeyConnectorUserKeyRequest) => Promise; } diff --git a/common/src/abstractions/environment.service.ts b/common/src/abstractions/environment.service.ts index 009851bedfd..db335016708 100644 --- a/common/src/abstractions/environment.service.ts +++ b/common/src/abstractions/environment.service.ts @@ -8,6 +8,7 @@ export type Urls = { icons?: string; notifications?: string; events?: string; + keyConnector?: string; }; export type PayPalConfig = { @@ -26,6 +27,7 @@ export abstract class EnvironmentService { getApiUrl: () => string; getIdentityUrl: () => string; getEventsUrl: () => string; + getKeyConnectorUrl: () => string; setUrlsFromStorage: () => Promise; setUrls: (urls: any, saveSettings?: boolean) => Promise; getUrls: () => Urls; diff --git a/common/src/abstractions/keyConnector.service.ts b/common/src/abstractions/keyConnector.service.ts new file mode 100644 index 00000000000..a4fbbf5ad91 --- /dev/null +++ b/common/src/abstractions/keyConnector.service.ts @@ -0,0 +1,14 @@ +import { Organization } from '../models/domain/organization'; + +export abstract class KeyConnectorService { + getAndSetKey: (url?: string) => Promise; + getManagingOrganization: () => Promise; + getUsesKeyConnector: () => Promise; + migrateUser: () => Promise; + userNeedsMigration: () => Promise; + setUsesKeyConnector: (enabled: boolean) => Promise; + setConvertAccountRequired: (status: boolean) => Promise; + getConvertAccountRequired: () => Promise; + removeConvertAccountRequired: () => Promise; + clear: () => Promise; +} diff --git a/common/src/abstractions/passwordReprompt.service.ts b/common/src/abstractions/passwordReprompt.service.ts index f0e694a063a..84ca84f539e 100644 --- a/common/src/abstractions/passwordReprompt.service.ts +++ b/common/src/abstractions/passwordReprompt.service.ts @@ -1,4 +1,5 @@ export abstract class PasswordRepromptService { protectedFields: () => string[]; showPasswordPrompt: () => Promise; + enabled: () => Promise; } diff --git a/common/src/abstractions/token.service.ts b/common/src/abstractions/token.service.ts index fbed6974c28..73c960ec343 100644 --- a/common/src/abstractions/token.service.ts +++ b/common/src/abstractions/token.service.ts @@ -26,4 +26,5 @@ export abstract class TokenService { getName: () => string; getPremium: () => boolean; getIssuer: () => string; + getIsExternal: () => boolean; } diff --git a/common/src/abstractions/user.service.ts b/common/src/abstractions/user.service.ts index 0cf6dc77c93..81c3da873d5 100644 --- a/common/src/abstractions/user.service.ts +++ b/common/src/abstractions/user.service.ts @@ -1,5 +1,6 @@ import { OrganizationData } from '../models/data/organizationData'; import { ProviderData } from '../models/data/providerData'; + import { Organization } from '../models/domain/organization'; import { Provider } from '../models/domain/provider'; diff --git a/common/src/abstractions/userVerification.service.ts b/common/src/abstractions/userVerification.service.ts new file mode 100644 index 00000000000..05c7a0903fd --- /dev/null +++ b/common/src/abstractions/userVerification.service.ts @@ -0,0 +1,9 @@ +import { SecretVerificationRequest } from '../models/request/secretVerificationRequest'; + +import { Verification } from '../types/verification'; + +export abstract class UserVerificationService { + buildRequest: (verification: Verification, + requestClass?: new () => T, alreadyHashed?: boolean) => Promise; + verifyUser: (verification: Verification) => Promise; +} diff --git a/common/src/enums/eventType.ts b/common/src/enums/eventType.ts index dabafa8182e..68fbc6b1432 100644 --- a/common/src/enums/eventType.ts +++ b/common/src/enums/eventType.ts @@ -8,6 +8,7 @@ export enum EventType { User_FailedLogIn2fa = 1006, User_ClientExportedVault = 1007, User_UpdatedTempPassword = 1008, + User_MigratedKeyToKeyConnector = 1009, Cipher_Created = 1100, Cipher_Updated = 1101, @@ -51,6 +52,10 @@ export enum EventType { Organization_PurgedVault = 1601, // Organization_ClientExportedVault = 1602, Organization_VaultAccessed = 1603, + Organization_EnabledSso = 1604, + Organization_DisabledSso = 1605, + Organization_EnabledKeyConnector = 1606, + Organization_DisabledKeyConnector = 1607, Policy_Updated = 1700, diff --git a/common/src/enums/verificationType.ts b/common/src/enums/verificationType.ts new file mode 100644 index 00000000000..254da418e2c --- /dev/null +++ b/common/src/enums/verificationType.ts @@ -0,0 +1,4 @@ +export enum VerificationType { + MasterPassword = 0, + OTP = 1, +} diff --git a/common/src/models/api/ssoConfigApi.ts b/common/src/models/api/ssoConfigApi.ts index ce7e5b833aa..9c260116bc5 100644 --- a/common/src/models/api/ssoConfigApi.ts +++ b/common/src/models/api/ssoConfigApi.ts @@ -37,8 +37,8 @@ enum Saml2SigningBehavior { export class SsoConfigApi extends BaseResponse { configType: SsoType; - useCryptoAgent: boolean; - cryptoAgentUrl: string; + useKeyConnector: boolean; + keyConnectorUrl: string; // OpenId authority: string; @@ -81,8 +81,8 @@ export class SsoConfigApi extends BaseResponse { this.configType = this.getResponseProperty('ConfigType'); - this.useCryptoAgent = this.getResponseProperty('UseCryptoAgent'); - this.cryptoAgentUrl = this.getResponseProperty('CryptoAgentUrl'); + this.useKeyConnector = this.getResponseProperty('UseKeyConnector'); + this.keyConnectorUrl = this.getResponseProperty('KeyConnectorUrl'); this.authority = this.getResponseProperty('Authority'); this.clientId = this.getResponseProperty('ClientId'); diff --git a/common/src/models/data/organizationData.ts b/common/src/models/data/organizationData.ts index 3d298a0b360..ed44822e8c3 100644 --- a/common/src/models/data/organizationData.ts +++ b/common/src/models/data/organizationData.ts @@ -33,6 +33,8 @@ export class OrganizationData { providerId: string; providerName: string; isProviderUser: boolean; + usesKeyConnector: boolean; + keyConnectorUrl: string; constructor(response: ProfileOrganizationResponse) { this.id = response.id; @@ -62,5 +64,7 @@ export class OrganizationData { this.hasPublicAndPrivateKeys = response.hasPublicAndPrivateKeys; this.providerId = response.providerId; this.providerName = response.providerName; + this.usesKeyConnector = response.usesKeyConnector; + this.keyConnectorUrl = response.keyConnectorUrl; } } diff --git a/common/src/models/domain/organization.ts b/common/src/models/domain/organization.ts index 8edec4ab75d..1ad1d6f763b 100644 --- a/common/src/models/domain/organization.ts +++ b/common/src/models/domain/organization.ts @@ -34,6 +34,8 @@ export class Organization { providerId: string; providerName: string; isProviderUser: boolean; + usesKeyConnector: boolean; + keyConnectorUrl: string; constructor(obj?: OrganizationData) { if (obj == null) { @@ -68,6 +70,8 @@ export class Organization { this.providerId = obj.providerId; this.providerName = obj.providerName; this.isProviderUser = obj.isProviderUser; + this.usesKeyConnector = obj.usesKeyConnector; + this.keyConnectorUrl = obj.keyConnectorUrl; } get canAccess() { diff --git a/common/src/models/request/account/setCryptoAgentKeyRequest.ts b/common/src/models/request/account/setKeyConnectorKeyRequest.ts similarity index 92% rename from common/src/models/request/account/setCryptoAgentKeyRequest.ts rename to common/src/models/request/account/setKeyConnectorKeyRequest.ts index 7f76af3d219..87b580d0b1d 100644 --- a/common/src/models/request/account/setCryptoAgentKeyRequest.ts +++ b/common/src/models/request/account/setKeyConnectorKeyRequest.ts @@ -2,7 +2,7 @@ import { KeysRequest } from '../keysRequest'; import { KdfType } from '../../../enums/kdfType'; -export class SetCryptoAgentKeyRequest { +export class SetKeyConnectorKeyRequest { key: string; keys: KeysRequest; kdf: KdfType; diff --git a/common/src/models/request/account/verifyOTPRequest.ts b/common/src/models/request/account/verifyOTPRequest.ts new file mode 100644 index 00000000000..8cc8f9850f0 --- /dev/null +++ b/common/src/models/request/account/verifyOTPRequest.ts @@ -0,0 +1,7 @@ +export class VerifyOTPRequest { + OTP: string; + + constructor(OTP: string) { + this.OTP = OTP; + } +} diff --git a/common/src/models/request/emailTokenRequest.ts b/common/src/models/request/emailTokenRequest.ts index 3e74669d4c7..90a806b6968 100644 --- a/common/src/models/request/emailTokenRequest.ts +++ b/common/src/models/request/emailTokenRequest.ts @@ -1,6 +1,6 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class EmailTokenRequest extends PasswordVerificationRequest { +export class EmailTokenRequest extends SecretVerificationRequest { newEmail: string; masterPasswordHash: string; } diff --git a/common/src/models/request/cryptoAgentUserKeyRequest.ts b/common/src/models/request/keyConnectorUserKeyRequest.ts similarity index 65% rename from common/src/models/request/cryptoAgentUserKeyRequest.ts rename to common/src/models/request/keyConnectorUserKeyRequest.ts index 87363a153ab..182de57f70a 100644 --- a/common/src/models/request/cryptoAgentUserKeyRequest.ts +++ b/common/src/models/request/keyConnectorUserKeyRequest.ts @@ -1,4 +1,4 @@ -export class CryptoAgentUserKeyRequest { +export class KeyConnectorUserKeyRequest { key: string; constructor(key: string) { diff --git a/common/src/models/request/passwordRequest.ts b/common/src/models/request/passwordRequest.ts index 4d877da6d48..0d639a09505 100644 --- a/common/src/models/request/passwordRequest.ts +++ b/common/src/models/request/passwordRequest.ts @@ -1,6 +1,6 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class PasswordRequest extends PasswordVerificationRequest { +export class PasswordRequest extends SecretVerificationRequest { newMasterPasswordHash: string; key: string; } diff --git a/common/src/models/request/passwordVerificationRequest.ts b/common/src/models/request/passwordVerificationRequest.ts deleted file mode 100644 index f9a038ac151..00000000000 --- a/common/src/models/request/passwordVerificationRequest.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class PasswordVerificationRequest { - masterPasswordHash: string; -} diff --git a/common/src/models/request/secretVerificationRequest.ts b/common/src/models/request/secretVerificationRequest.ts new file mode 100644 index 00000000000..554aa1c6026 --- /dev/null +++ b/common/src/models/request/secretVerificationRequest.ts @@ -0,0 +1,4 @@ +export class SecretVerificationRequest { + masterPasswordHash: string; + otp: string; +} diff --git a/common/src/models/request/twoFactorEmailRequest.ts b/common/src/models/request/twoFactorEmailRequest.ts index ced9288ffd2..6eeb018520f 100644 --- a/common/src/models/request/twoFactorEmailRequest.ts +++ b/common/src/models/request/twoFactorEmailRequest.ts @@ -1,11 +1,5 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class TwoFactorEmailRequest extends PasswordVerificationRequest { +export class TwoFactorEmailRequest extends SecretVerificationRequest { email: string; - - constructor(email: string, masterPasswordHash: string) { - super(); - this.masterPasswordHash = masterPasswordHash; - this.email = email; - } } diff --git a/common/src/models/request/twoFactorProviderRequest.ts b/common/src/models/request/twoFactorProviderRequest.ts index 23a47f9e0e2..c91385974fb 100644 --- a/common/src/models/request/twoFactorProviderRequest.ts +++ b/common/src/models/request/twoFactorProviderRequest.ts @@ -1,7 +1,7 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; -export class TwoFactorProviderRequest extends PasswordVerificationRequest { +export class TwoFactorProviderRequest extends SecretVerificationRequest { type: TwoFactorProviderType; } diff --git a/common/src/models/request/twoFactorRecoveryRequest.ts b/common/src/models/request/twoFactorRecoveryRequest.ts index ddd7c59ad7c..3352f327c02 100644 --- a/common/src/models/request/twoFactorRecoveryRequest.ts +++ b/common/src/models/request/twoFactorRecoveryRequest.ts @@ -1,6 +1,6 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class TwoFactorRecoveryRequest extends PasswordVerificationRequest { +export class TwoFactorRecoveryRequest extends SecretVerificationRequest { recoveryCode: string; email: string; } diff --git a/common/src/models/request/updateTwoFactorAuthenticatorRequest.ts b/common/src/models/request/updateTwoFactorAuthenticatorRequest.ts index 2dd6a96ac93..fa0a96c040f 100644 --- a/common/src/models/request/updateTwoFactorAuthenticatorRequest.ts +++ b/common/src/models/request/updateTwoFactorAuthenticatorRequest.ts @@ -1,6 +1,6 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class UpdateTwoFactorAuthenticatorRequest extends PasswordVerificationRequest { +export class UpdateTwoFactorAuthenticatorRequest extends SecretVerificationRequest { token: string; key: string; } diff --git a/common/src/models/request/updateTwoFactorDuoRequest.ts b/common/src/models/request/updateTwoFactorDuoRequest.ts index 1777f39ae80..9465e2f9972 100644 --- a/common/src/models/request/updateTwoFactorDuoRequest.ts +++ b/common/src/models/request/updateTwoFactorDuoRequest.ts @@ -1,6 +1,6 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class UpdateTwoFactorDuoRequest extends PasswordVerificationRequest { +export class UpdateTwoFactorDuoRequest extends SecretVerificationRequest { integrationKey: string; secretKey: string; host: string; diff --git a/common/src/models/request/updateTwoFactorEmailRequest.ts b/common/src/models/request/updateTwoFactorEmailRequest.ts index dabeb7d9d3e..ce2fedb55eb 100644 --- a/common/src/models/request/updateTwoFactorEmailRequest.ts +++ b/common/src/models/request/updateTwoFactorEmailRequest.ts @@ -1,6 +1,6 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class UpdateTwoFactorEmailRequest extends PasswordVerificationRequest { +export class UpdateTwoFactorEmailRequest extends SecretVerificationRequest { token: string; email: string; } diff --git a/common/src/models/request/updateTwoFactorWebAuthnDeleteRequest.ts b/common/src/models/request/updateTwoFactorWebAuthnDeleteRequest.ts index 943d9e414fd..8fa5fcd03b9 100644 --- a/common/src/models/request/updateTwoFactorWebAuthnDeleteRequest.ts +++ b/common/src/models/request/updateTwoFactorWebAuthnDeleteRequest.ts @@ -1,5 +1,5 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class UpdateTwoFactorWebAuthnDeleteRequest extends PasswordVerificationRequest { +export class UpdateTwoFactorWebAuthnDeleteRequest extends SecretVerificationRequest { id: number; } diff --git a/common/src/models/request/updateTwoFactorWebAuthnRequest.ts b/common/src/models/request/updateTwoFactorWebAuthnRequest.ts index ef9308eb5cd..09976c3114d 100644 --- a/common/src/models/request/updateTwoFactorWebAuthnRequest.ts +++ b/common/src/models/request/updateTwoFactorWebAuthnRequest.ts @@ -1,6 +1,6 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class UpdateTwoFactorWebAuthnRequest extends PasswordVerificationRequest { +export class UpdateTwoFactorWebAuthnRequest extends SecretVerificationRequest { deviceResponse: PublicKeyCredential; name: string; id: number; diff --git a/common/src/models/request/updateTwoFactorYubioOtpRequest.ts b/common/src/models/request/updateTwoFactorYubioOtpRequest.ts index 4ad0696f0ce..29a9dd8145c 100644 --- a/common/src/models/request/updateTwoFactorYubioOtpRequest.ts +++ b/common/src/models/request/updateTwoFactorYubioOtpRequest.ts @@ -1,6 +1,6 @@ -import { PasswordVerificationRequest } from './passwordVerificationRequest'; +import { SecretVerificationRequest } from './secretVerificationRequest'; -export class UpdateTwoFactorYubioOtpRequest extends PasswordVerificationRequest { +export class UpdateTwoFactorYubioOtpRequest extends SecretVerificationRequest { key1: string; key2: string; key3: string; diff --git a/common/src/models/response/identityTokenResponse.ts b/common/src/models/response/identityTokenResponse.ts index aa2fe8fdd2d..7b128f44c49 100644 --- a/common/src/models/response/identityTokenResponse.ts +++ b/common/src/models/response/identityTokenResponse.ts @@ -15,7 +15,7 @@ export class IdentityTokenResponse extends BaseResponse { kdf: KdfType; kdfIterations: number; forcePasswordReset: boolean; - cryptoAgentUrl: string; + keyConnectorUrl: string; constructor(response: any) { super(response); @@ -31,6 +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'); + this.keyConnectorUrl = this.getResponseProperty('KeyConnectorUrl'); } } diff --git a/common/src/models/response/cryptoAgentUserKeyResponse.ts b/common/src/models/response/keyConnectorUserKeyResponse.ts similarity index 74% rename from common/src/models/response/cryptoAgentUserKeyResponse.ts rename to common/src/models/response/keyConnectorUserKeyResponse.ts index 0397388eaf9..4da60be3b65 100644 --- a/common/src/models/response/cryptoAgentUserKeyResponse.ts +++ b/common/src/models/response/keyConnectorUserKeyResponse.ts @@ -1,6 +1,6 @@ import { BaseResponse } from './baseResponse'; -export class CryptoAgentUserKeyResponse extends BaseResponse { +export class KeyConnectorUserKeyResponse extends BaseResponse { key: string; constructor(response: any) { diff --git a/common/src/models/response/profileOrganizationResponse.ts b/common/src/models/response/profileOrganizationResponse.ts index b70d956756d..d4c77792afb 100644 --- a/common/src/models/response/profileOrganizationResponse.ts +++ b/common/src/models/response/profileOrganizationResponse.ts @@ -33,6 +33,8 @@ export class ProfileOrganizationResponse extends BaseResponse { userId: string; providerId: string; providerName: string; + usesKeyConnector: boolean; + keyConnectorUrl: string; constructor(response: any) { super(response); @@ -64,5 +66,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.userId = this.getResponseProperty('UserId'); this.providerId = this.getResponseProperty('ProviderId'); this.providerName = this.getResponseProperty('ProviderName'); + this.usesKeyConnector = this.getResponseProperty('UsesKeyConnector') ?? false; + this.keyConnectorUrl = this.getResponseProperty('KeyConnectorUrl'); } } diff --git a/common/src/models/response/profileProviderOrganizationResponse.ts b/common/src/models/response/profileProviderOrganizationResponse.ts index d9382da3ab1..dcf24561d80 100644 --- a/common/src/models/response/profileProviderOrganizationResponse.ts +++ b/common/src/models/response/profileProviderOrganizationResponse.ts @@ -1,68 +1,8 @@ -import { BaseResponse } from './baseResponse'; - -import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; -import { OrganizationUserType } from '../../enums/organizationUserType'; -import { PermissionsApi } from '../api/permissionsApi'; - -export class ProfileProviderOrganizationResponse extends BaseResponse { - id: string; - name: string; - usePolicies: boolean; - useGroups: boolean; - useDirectory: boolean; - useEvents: boolean; - useTotp: boolean; - use2fa: boolean; - useApi: boolean; - useSso: boolean; - useResetPassword: boolean; - selfHost: boolean; - usersGetPremium: boolean; - seats: number; - maxCollections: number; - maxStorageGb?: number; - key: string; - hasPublicAndPrivateKeys: boolean; - status: OrganizationUserStatusType; - type: OrganizationUserType; - enabled: boolean; - ssoBound: boolean; - identifier: string; - permissions: PermissionsApi; - resetPasswordEnrolled: boolean; - userId: string; - providerId: string; - providerName: string; +import { ProfileOrganizationResponse } from './profileOrganizationResponse'; +export class ProfileProviderOrganizationResponse extends ProfileOrganizationResponse { constructor(response: any) { super(response); - this.id = this.getResponseProperty('Id'); - this.name = this.getResponseProperty('Name'); - this.usePolicies = this.getResponseProperty('UsePolicies'); - this.useGroups = this.getResponseProperty('UseGroups'); - this.useDirectory = this.getResponseProperty('UseDirectory'); - this.useEvents = this.getResponseProperty('UseEvents'); - this.useTotp = this.getResponseProperty('UseTotp'); - this.use2fa = this.getResponseProperty('Use2fa'); - this.useApi = this.getResponseProperty('UseApi'); - this.useSso = this.getResponseProperty('UseSso'); - this.useResetPassword = this.getResponseProperty('UseResetPassword'); - this.selfHost = this.getResponseProperty('SelfHost'); - this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); - this.seats = this.getResponseProperty('Seats'); - this.maxCollections = this.getResponseProperty('MaxCollections'); - this.maxStorageGb = this.getResponseProperty('MaxStorageGb'); - this.key = this.getResponseProperty('Key'); - this.hasPublicAndPrivateKeys = this.getResponseProperty('HasPublicAndPrivateKeys'); - this.status = this.getResponseProperty('Status'); - this.type = this.getResponseProperty('Type'); - this.enabled = this.getResponseProperty('Enabled'); - this.ssoBound = this.getResponseProperty('SsoBound'); - this.identifier = this.getResponseProperty('Identifier'); - this.permissions = new PermissionsApi(this.getResponseProperty('permissions')); - this.resetPasswordEnrolled = this.getResponseProperty('ResetPasswordEnrolled'); - this.userId = this.getResponseProperty('UserId'); - this.providerId = this.getResponseProperty('ProviderId'); - this.providerName = this.getResponseProperty('ProviderName'); + this.usesKeyConnector = false; } } diff --git a/common/src/models/response/profileResponse.ts b/common/src/models/response/profileResponse.ts index 1c6e64faeef..913ff820fc2 100644 --- a/common/src/models/response/profileResponse.ts +++ b/common/src/models/response/profileResponse.ts @@ -16,6 +16,7 @@ export class ProfileResponse extends BaseResponse { privateKey: string; securityStamp: string; forcePasswordReset: boolean; + usesKeyConnector: boolean; organizations: ProfileOrganizationResponse[] = []; providers: ProfileProviderResponse[] = []; providerOrganizations: ProfileProviderOrganizationResponse[] = []; @@ -34,6 +35,7 @@ export class ProfileResponse extends BaseResponse { this.privateKey = this.getResponseProperty('PrivateKey'); this.securityStamp = this.getResponseProperty('SecurityStamp'); this.forcePasswordReset = this.getResponseProperty('ForcePasswordReset') ?? false; + this.usesKeyConnector = this.getResponseProperty('UsesKeyConnector') ?? false; const organizations = this.getResponseProperty('Organizations'); if (organizations != null) { diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index 81d19a32b37..b7fff0ce8eb 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -52,7 +52,6 @@ import { OrganizationUserUpdateGroupsRequest } from '../models/request/organizat import { OrganizationUserUpdateRequest } from '../models/request/organizationUserUpdateRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; -import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; import { PolicyRequest } from '../models/request/policyRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; @@ -68,6 +67,7 @@ import { ProviderUserInviteRequest } from '../models/request/provider/providerUs import { ProviderUserUpdateRequest } from '../models/request/provider/providerUserUpdateRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; +import { SecretVerificationRequest } from '../models/request/secretVerificationRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; import { SendAccessRequest } from '../models/request/sendAccessRequest'; import { SendRequest } from '../models/request/sendRequest'; @@ -166,9 +166,10 @@ 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 { SetKeyConnectorKeyRequest } from '../models/request/account/setKeyConnectorKeyRequest'; +import { VerifyOTPRequest } from '../models/request/account/verifyOTPRequest'; +import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; +import { KeyConnectorUserKeyResponse } from '../models/response/keyConnectorUserKeyResponse'; import { SendAccessView } from '../models/view/sendAccessView'; export class ApiService implements ApiServiceAbstraction { @@ -292,15 +293,15 @@ 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); + postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise { + return this.send('POST', '/accounts/set-key-connector-key', request, true, false); } - postSecurityStamp(request: PasswordVerificationRequest): Promise { + postSecurityStamp(request: SecretVerificationRequest): Promise { return this.send('POST', '/accounts/security-stamp', request, true, false); } - deleteAccount(request: PasswordVerificationRequest): Promise { + deleteAccount(request: SecretVerificationRequest): Promise { return this.send('DELETE', '/accounts', request, true, false); } @@ -363,7 +364,7 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/verify-email-token', request, false, false); } - postAccountVerifyPassword(request: PasswordVerificationRequest): Promise { + postAccountVerifyPassword(request: SecretVerificationRequest): Promise { return this.send('POST', '/accounts/verify-password', request, true, false); } @@ -387,12 +388,12 @@ export class ApiService implements ApiServiceAbstraction { return this.send('GET', '/accounts/sso/user-identifier', null, true, true); } - async postUserApiKey(id: string, request: PasswordVerificationRequest): Promise { + async postUserApiKey(id: string, request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/accounts/api-key', request, true, true); return new ApiKeyResponse(r); } - async postUserRotateApiKey(id: string, request: PasswordVerificationRequest): Promise { + async postUserRotateApiKey(id: string, request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/accounts/rotate-api-key', request, true, true); return new ApiKeyResponse(r); } @@ -401,6 +402,18 @@ export class ApiService implements ApiServiceAbstraction { return this.send('PUT', '/accounts/update-temp-password', request, true, false); } + postAccountRequestOTP(): Promise { + return this.send('POST', '/accounts/request-otp', null, true, false); + } + + postAccountVerifyOTP(request: VerifyOTPRequest): Promise { + return this.send('POST', '/accounts/verify-otp', request, true, false); + } + + postConvertToKeyConnector(): Promise { + return this.send('POST', '/accounts/convert-to-key-connector', null, true, false); + } + // Folder APIs async getFolder(id: string): Promise { @@ -573,7 +586,7 @@ export class ApiService implements ApiServiceAbstraction { return this.send('PUT', '/ciphers/' + id + '/collections-admin', request, true, false); } - postPurgeCiphers(request: PasswordVerificationRequest, organizationId: string = null): Promise { + postPurgeCiphers(request: SecretVerificationRequest, organizationId: string = null): Promise { let path = '/ciphers/purge'; if (organizationId != null) { path += '?organizationId=' + organizationId; @@ -939,44 +952,44 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, TwoFactorProviderResponse); } - async getTwoFactorAuthenticator(request: PasswordVerificationRequest): Promise { + async getTwoFactorAuthenticator(request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-authenticator', request, true, true); return new TwoFactorAuthenticatorResponse(r); } - async getTwoFactorEmail(request: PasswordVerificationRequest): Promise { + async getTwoFactorEmail(request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-email', request, true, true); return new TwoFactorEmailResponse(r); } - async getTwoFactorDuo(request: PasswordVerificationRequest): Promise { + async getTwoFactorDuo(request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-duo', request, true, true); return new TwoFactorDuoResponse(r); } async getTwoFactorOrganizationDuo(organizationId: string, - request: PasswordVerificationRequest): Promise { + request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/organizations/' + organizationId + '/two-factor/get-duo', request, true, true); return new TwoFactorDuoResponse(r); } - async getTwoFactorYubiKey(request: PasswordVerificationRequest): Promise { + async getTwoFactorYubiKey(request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-yubikey', request, true, true); return new TwoFactorYubiKeyResponse(r); } - async getTwoFactorWebAuthn(request: PasswordVerificationRequest): Promise { + async getTwoFactorWebAuthn(request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-webauthn', request, true, true); return new TwoFactorWebAuthnResponse(r); } - async getTwoFactorWebAuthnChallenge(request: PasswordVerificationRequest): Promise { + async getTwoFactorWebAuthnChallenge(request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-webauthn-challenge', request, true, true); return new ChallengeResponse(r); } - async getTwoFactorRecover(request: PasswordVerificationRequest): Promise { + async getTwoFactorRecover(request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-recover', request, true, true); return new TwoFactorRecoverResponse(r); } @@ -1187,12 +1200,12 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/organizations/' + id + '/license', data, true, false); } - async postOrganizationApiKey(id: string, request: PasswordVerificationRequest): Promise { + async postOrganizationApiKey(id: string, request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/organizations/' + id + '/api-key', request, true, true); return new ApiKeyResponse(r); } - async postOrganizationRotateApiKey(id: string, request: PasswordVerificationRequest): Promise { + async postOrganizationRotateApiKey(id: string, request: SecretVerificationRequest): Promise { const r = await this.send('POST', '/organizations/' + id + '/rotate-api-key', request, true, true); return new ApiKeyResponse(r); } @@ -1237,7 +1250,7 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/organizations/' + id + '/reinstate', null, true, false); } - deleteOrganization(id: string, request: PasswordVerificationRequest): Promise { + deleteOrganization(id: string, request: SecretVerificationRequest): Promise { return this.send('DELETE', '/organizations/' + id, request, true, false); } @@ -1436,12 +1449,12 @@ export class ApiService implements ApiServiceAbstraction { return r as string; } - // Crypto Agent + // Key Connector - async getUserKeyFromCryptoAgent(cryptoAgentUrl: string): Promise { + async getUserKeyFromKeyConnector(keyConnectorUrl: string): Promise { const authHeader = await this.getActiveBearerToken(); - const response = await this.fetch(new Request(cryptoAgentUrl + '/user-keys', { + const response = await this.fetch(new Request(keyConnectorUrl + '/user-keys', { cache: 'no-store', method: 'GET', headers: new Headers({ @@ -1455,13 +1468,13 @@ export class ApiService implements ApiServiceAbstraction { return Promise.reject(error); } - return new CryptoAgentUserKeyResponse(await response.json()); + return new KeyConnectorUserKeyResponse(await response.json()); } - async postUserKeyToCryptoAgent(cryptoAgentUrl: string, request: CryptoAgentUserKeyRequest): Promise { + async postUserKeyToKeyConnector(keyConnectorUrl: string, request: KeyConnectorUserKeyRequest): Promise { const authHeader = await this.getActiveBearerToken(); - const response = await this.fetch(new Request(cryptoAgentUrl + '/user-keys', { + const response = await this.fetch(new Request(keyConnectorUrl + '/user-keys', { cache: 'no-store', method: 'POST', headers: new Headers({ diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index f92e2df8e18..5c5ca225f07 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -5,9 +5,9 @@ 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 { SetKeyConnectorKeyRequest } from '../models/request/account/setKeyConnectorKeyRequest'; import { DeviceRequest } from '../models/request/deviceRequest'; +import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; import { TokenRequest } from '../models/request/tokenRequest'; @@ -20,7 +20,9 @@ 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 { EnvironmentService } from '../abstractions/environment.service'; import { I18nService } from '../abstractions/i18n.service'; +import { KeyConnectorService } from '../abstractions/keyConnector.service'; import { LogService } from '../abstractions/log.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; @@ -101,7 +103,8 @@ export class AuthService implements AuthServiceAbstraction { protected appIdService: AppIdService, private i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, private vaultTimeoutService: VaultTimeoutService, private logService: LogService, - private cryptoFunctionService: CryptoFunctionService, private setCryptoKeys = true) { + private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService, + private keyConnectorService: KeyConnectorService, private setCryptoKeys = true) { } init() { @@ -365,16 +368,10 @@ 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'); - } + if (tokenResponse.keyConnectorUrl != null) { + await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl); + } else if (this.environmentService.getKeyConnectorUrl() != null) { + await this.keyConnectorService.getAndSetKey(); } await this.cryptoService.setEncKey(tokenResponse.key); @@ -391,11 +388,11 @@ export class AuthService implements AuthServiceAbstraction { } await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); - } else if (tokenResponse.cryptoAgentUrl != null) { + } else if (tokenResponse.keyConnectorUrl != 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); + const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64); await this.cryptoService.setKey(k); const encKey = await this.cryptoService.makeEncKey(k); @@ -404,16 +401,16 @@ export class AuthService implements AuthServiceAbstraction { const [pubKey, privKey] = await this.cryptoService.makeKeyPair(); try { - await this.apiService.postUserKeyToCryptoAgent(tokenResponse.cryptoAgentUrl, cryptoAgentRequest); + await this.apiService.postUserKeyToKeyConnector(tokenResponse.keyConnectorUrl, keyConnectorRequest); } catch (e) { - throw new Error('Unable to reach crypto agent'); + throw new Error('Unable to reach key connector'); } const keys = new KeysRequest(pubKey, privKey.encryptedString); - const setPasswordRequest = new SetCryptoAgentKeyRequest( + const setPasswordRequest = new SetKeyConnectorKeyRequest( encKey[1].encryptedString, tokenResponse.kdf, tokenResponse.kdfIterations, orgId, keys ); - await this.apiService.postSetCryptoAgentKey(setPasswordRequest); + await this.apiService.postSetKeyConnectorKey(setPasswordRequest); } } diff --git a/common/src/services/environment.service.ts b/common/src/services/environment.service.ts index 9f5a6f64a7c..8d10995a001 100644 --- a/common/src/services/environment.service.ts +++ b/common/src/services/environment.service.ts @@ -19,6 +19,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { private iconsUrl: string; private notificationsUrl: string; private eventsUrl: string; + private keyConnectorUrl: string; constructor(private storageService: StorageService) {} @@ -103,6 +104,10 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { return 'https://events.bitwarden.com'; } + getKeyConnectorUrl() { + return this.keyConnectorUrl; + } + async setUrlsFromStorage(): Promise { const urlsObj: any = await this.storageService.get(ConstantsService.environmentUrlsKey); const urls = urlsObj || { @@ -113,6 +118,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { notifications: null, events: null, webVault: null, + keyConnector: null, }; const envUrls = new EnvironmentUrls(); @@ -128,6 +134,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.iconsUrl = urls.icons; this.notificationsUrl = urls.notifications; this.eventsUrl = envUrls.events = urls.events; + this.keyConnectorUrl = urls.keyConnector; } async setUrls(urls: Urls, saveSettings: boolean = true): Promise { @@ -138,6 +145,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { urls.icons = this.formatUrl(urls.icons); urls.notifications = this.formatUrl(urls.notifications); urls.events = this.formatUrl(urls.events); + urls.keyConnector = this.formatUrl(urls.keyConnector); if (saveSettings) { await this.storageService.save(ConstantsService.environmentUrlsKey, { @@ -148,6 +156,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { icons: urls.icons, notifications: urls.notifications, events: urls.events, + keyConnector: urls.keyConnector, }); } @@ -158,6 +167,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.iconsUrl = urls.icons; this.notificationsUrl = urls.notifications; this.eventsUrl = urls.events; + this.keyConnectorUrl = urls.keyConnector; this.urlsSubject.next(urls); @@ -173,6 +183,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { icons: this.iconsUrl, notifications: this.notificationsUrl, events: this.eventsUrl, + keyConnector: this.keyConnectorUrl, }; } diff --git a/common/src/services/keyConnector.service.ts b/common/src/services/keyConnector.service.ts new file mode 100644 index 00000000000..68aac9a21ec --- /dev/null +++ b/common/src/services/keyConnector.service.ts @@ -0,0 +1,106 @@ +import { ApiService } from '../abstractions/api.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { EnvironmentService } from '../abstractions/environment.service'; +import { KeyConnectorService as KeyConnectorServiceAbstraction } from '../abstractions/keyConnector.service'; +import { LogService } from '../abstractions/log.service'; +import { StorageService } from '../abstractions/storage.service'; +import { TokenService } from '../abstractions/token.service'; +import { UserService } from '../abstractions/user.service'; + +import { OrganizationUserType } from '../enums/organizationUserType'; + +import { Utils } from '../misc/utils'; + +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; + +const Keys = { + usesKeyConnector: 'usesKeyConnector', + convertAccountToKeyConnector: 'convertAccountToKeyConnector', +}; + +export class KeyConnectorService implements KeyConnectorServiceAbstraction { + private usesKeyConnector?: boolean = null; + + constructor(private storageService: StorageService, private userService: UserService, + private cryptoService: CryptoService, private apiService: ApiService, + private environmentService: EnvironmentService, private tokenService: TokenService, + private logService: LogService) { } + + setUsesKeyConnector(usesKeyConnector: boolean) { + this.usesKeyConnector = usesKeyConnector; + return this.storageService.save(Keys.usesKeyConnector, usesKeyConnector); + } + + async getUsesKeyConnector(): Promise { + return this.usesKeyConnector ??= await this.storageService.get(Keys.usesKeyConnector); + } + + async userNeedsMigration() { + const loggedInUsingSso = this.tokenService.getIsExternal(); + const requiredByOrganization = await this.getManagingOrganization() != null; + const userIsNotUsingKeyConnector = !await this.getUsesKeyConnector(); + + return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector; + } + + async migrateUser() { + const organization = await this.getManagingOrganization(); + const key = await this.cryptoService.getKey(); + + try { + const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64); + await this.apiService.postUserKeyToKeyConnector(organization.keyConnectorUrl, keyConnectorRequest); + } catch (e) { + throw new Error('Unable to reach key connector'); + } + + await this.apiService.postConvertToKeyConnector(); + } + + async getAndSetKey(url?: string) { + if (url == null) { + url = this.environmentService.getKeyConnectorUrl(); + } + + if (url == null) { + throw new Error('No Key Connector URL found.'); + } + + try { + const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url); + 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 key connector'); + } + } + + async getManagingOrganization() { + const orgs = await this.userService.getAllOrganizations(); + return orgs.find(o => + o.usesKeyConnector && + o.type !== OrganizationUserType.Admin && + o.type !== OrganizationUserType.Owner && + !o.isProviderUser); + } + + async setConvertAccountRequired(status: boolean) { + await this.storageService.save(Keys.convertAccountToKeyConnector, status); + } + + async getConvertAccountRequired(): Promise { + return await this.storageService.get(Keys.convertAccountToKeyConnector); + } + + async removeConvertAccountRequired() { + await this.storageService.remove(Keys.convertAccountToKeyConnector); + } + + async clear() { + await this.removeConvertAccountRequired(); + } +} diff --git a/common/src/services/sync.service.ts b/common/src/services/sync.service.ts index 219a5765b90..4b7ee9c4db8 100644 --- a/common/src/services/sync.service.ts +++ b/common/src/services/sync.service.ts @@ -3,6 +3,7 @@ import { CipherService } from '../abstractions/cipher.service'; import { CollectionService } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService } from '../abstractions/folder.service'; +import { KeyConnectorService } from '../abstractions/keyConnector.service'; import { LogService } from '../abstractions/log.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PolicyService } from '../abstractions/policy.service'; @@ -10,6 +11,7 @@ import { SendService } from '../abstractions/send.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; +import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; import { CipherData } from '../models/data/cipherData'; @@ -46,6 +48,7 @@ export class SyncService implements SyncServiceAbstraction { private collectionService: CollectionService, private storageService: StorageService, private messagingService: MessagingService, private policyService: PolicyService, private sendService: SendService, private logService: LogService, + private tokenService: TokenService, private keyConnectorService: KeyConnectorService, private logoutCallback: (expired: boolean) => Promise) { } @@ -299,6 +302,7 @@ export class SyncService implements SyncServiceAbstraction { await this.userService.setSecurityStamp(response.securityStamp); await this.userService.setEmailVerified(response.emailVerified); await this.userService.setForcePasswordReset(response.forcePasswordReset); + await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); const organizations: { [id: string]: OrganizationData; } = {}; response.organizations.forEach(o => { @@ -316,6 +320,13 @@ export class SyncService implements SyncServiceAbstraction { organizations[o.id].isProviderUser = true; } }); + + if (await this.keyConnectorService.userNeedsMigration()) { + this.messagingService.send('convertAccountToKeyConnector'); + } else { + this.keyConnectorService.removeConvertAccountRequired(); + } + return Promise.all([ this.userService.replaceOrganizations(organizations), this.userService.replaceProviders(providers), diff --git a/common/src/services/token.service.ts b/common/src/services/token.service.ts index 0c3b9c40159..58f42e7d4ac 100644 --- a/common/src/services/token.service.ts +++ b/common/src/services/token.service.ts @@ -243,6 +243,15 @@ export class TokenService implements TokenServiceAbstraction { return decoded.iss as string; } + getIsExternal(): boolean { + const decoded = this.decodeToken(); + if (!Array.isArray(decoded.amr)) { + throw new Error('No amr found'); + } + + return decoded.amr.includes('external'); + } + private async storeTokenValue(key: string, value: string) { if (await this.skipTokenStorage()) { // if we have a vault timeout and the action is log out, don't store token diff --git a/common/src/services/user.service.ts b/common/src/services/user.service.ts index f031babb6d1..658179f94b7 100644 --- a/common/src/services/user.service.ts +++ b/common/src/services/user.service.ts @@ -6,6 +6,7 @@ import { OrganizationData } from '../models/data/organizationData'; import { Organization } from '../models/domain/organization'; import { KdfType } from '../enums/kdfType'; + import { ProviderData } from '../models/data/providerData'; import { Provider } from '../models/domain/provider'; diff --git a/common/src/services/userVerification.service.ts b/common/src/services/userVerification.service.ts new file mode 100644 index 00000000000..7910f44a169 --- /dev/null +++ b/common/src/services/userVerification.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; + +import { UserVerificationService as UserVerificationServiceAbstraction } from '../abstractions/userVerification.service'; + +import { ApiService } from '../abstractions/api.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { I18nService } from '../abstractions/i18n.service'; +import { LogService } from '../abstractions/log.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; + +import { VerificationType } from '../enums/verificationType'; + +import { VerifyOTPRequest } from '../models/request/account/verifyOTPRequest'; +import { SecretVerificationRequest } from '../models/request/secretVerificationRequest'; + +import { Verification } from '../types/verification'; + +@Injectable() +export class UserVerificationService implements UserVerificationServiceAbstraction { + constructor(private cryptoService: CryptoService, private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, private apiService: ApiService, + private logService: LogService) { } + + async buildRequest(verification: Verification, + requestClass?: new () => T, alreadyHashed?: boolean) { + if (verification?.secret == null || verification.secret === '') { + throw new Error('No secret provided for verification.'); + } + + const request = requestClass != null + ? new requestClass() + : new SecretVerificationRequest() as T; + + if (verification.type === VerificationType.OTP) { + request.otp = verification.secret; + } else { + request.masterPasswordHash = alreadyHashed + ? verification.secret + : await this.cryptoService.hashPassword(verification.secret, null); + } + + return request; + } + + async verifyUser(verification: Verification): Promise { + if (verification?.secret == null || verification.secret === '') { + throw new Error('No secret provided for verification.'); + } + + if (verification.type === VerificationType.OTP) { + const request = new VerifyOTPRequest(verification.secret); + try { + await this.apiService.postAccountVerifyOTP(request); + } catch (e) { + this.logService.error(e); + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidVerificationCode')); + return false; + } + } else { + const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(verification.secret, null); + if (!passwordValid) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidMasterPassword')); + return false; + } + } + return true; + } +} diff --git a/common/src/services/vaultTimeout.service.ts b/common/src/services/vaultTimeout.service.ts index 907f3829953..54d288b1550 100644 --- a/common/src/services/vaultTimeout.service.ts +++ b/common/src/services/vaultTimeout.service.ts @@ -4,6 +4,7 @@ import { CipherService } from '../abstractions/cipher.service'; import { CollectionService } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService } from '../abstractions/folder.service'; +import { KeyConnectorService } from '../abstractions/keyConnector.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { PolicyService } from '../abstractions/policy.service'; @@ -28,6 +29,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { protected platformUtilsService: PlatformUtilsService, private storageService: StorageService, private messagingService: MessagingService, private searchService: SearchService, private userService: UserService, private tokenService: TokenService, private policyService: PolicyService, + private keyConnectorService: KeyConnectorService, private lockedCallback: () => Promise = null, private loggedOutCallback: () => Promise = null) { } @@ -98,6 +100,15 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } + if (await this.keyConnectorService.getUsesKeyConnector()) { + const pinSet = await this.isPinLockSet(); + const pinLock = (pinSet[0] && this.pinProtectedKey != null) || pinSet[1]; + + if (!pinLock && !await this.isBiometricLockSet()) { + await this.logOut(); + } + } + this.biometricLocked = true; this.everBeenUnlocked = true; await this.cryptoService.clearKey(false); diff --git a/common/src/types/verification.ts b/common/src/types/verification.ts new file mode 100644 index 00000000000..72ee8b0a205 --- /dev/null +++ b/common/src/types/verification.ts @@ -0,0 +1,6 @@ +import { VerificationType } from '../enums/verificationType'; + +export type Verification = { + type: VerificationType, + secret: string, +}; diff --git a/node/src/cli/commands/login.command.ts b/node/src/cli/commands/login.command.ts index 6ed398e4704..7cdb607eb0a 100644 --- a/node/src/cli/commands/login.command.ts +++ b/node/src/cli/commands/login.command.ts @@ -14,6 +14,7 @@ import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PolicyService } from 'jslib-common/abstractions/policy.service'; @@ -22,6 +23,7 @@ import { UserService } from 'jslib-common/abstractions/user.service'; import { Response } from '../models/response'; +import { KeyConnectorUserKeyRequest } from 'jslib-common/models/request/keyConnectorUserKeyRequest'; import { UpdateTempPasswordRequest } from 'jslib-common/models/request/updateTempPasswordRequest'; import { MessageResponse } from '../models/response/messageResponse'; @@ -48,7 +50,8 @@ export class LoginCommand { protected passwordGenerationService: PasswordGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected platformUtilsService: PlatformUtilsService, protected userService: UserService, protected cryptoService: CryptoService, - protected policyService: PolicyService, clientId: string, private syncService: SyncService) { + protected policyService: PolicyService, clientId: string, private syncService: SyncService, + protected keyConnectorService: KeyConnectorService) { this.clientId = clientId; } @@ -57,6 +60,7 @@ export class LoginCommand { let ssoCodeVerifier: string = null; let ssoCode: string = null; + let orgIdentifier: string = null; let clientId: string = null; let clientSecret: string = null; @@ -79,7 +83,9 @@ export class LoginCommand { const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256'); const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); try { - ssoCode = await this.getSsoCode(codeChallenge, state); + const ssoParams = await this.openSsoPrompt(codeChallenge, state); + ssoCode = ssoParams.ssoCode; + orgIdentifier = ssoParams.orgIdentifier; } catch { return Response.badRequest('Something went wrong. Try again.'); } @@ -151,7 +157,8 @@ export class LoginCommand { if (clientId != null && clientSecret != null) { response = await this.authService.logInApiKey(clientId, clientSecret); } else if (ssoCode != null && ssoCodeVerifier != null) { - response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri, null); + response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri, + orgIdentifier); } else { response = await this.authService.logIn(email, password); } @@ -220,8 +227,9 @@ export class LoginCommand { if (twoFactorToken == null && response.twoFactorProviders.size > 1 && selectedProvider.type === TwoFactorProviderType.Email) { - const emailReq = new TwoFactorEmailRequest(this.authService.email, - this.authService.masterPasswordHash); + const emailReq = new TwoFactorEmailRequest(); + emailReq.email = this.authService.email; + emailReq.masterPasswordHash = this.authService.masterPasswordHash; await this.apiService.postTwoFactorEmail(emailReq); } @@ -254,9 +262,16 @@ export class LoginCommand { ' through the web vault to set your master password.'); } + // Full sync required for the reset password and key connector checks + await this.syncService.fullSync(true); + + // Handle converting to Key Connector if required + if (await this.keyConnectorService.userNeedsMigration()) { + return await this.migrateToKeyConnector(); + } + // Handle Updating Temp Password if NOT using an API Key for authentication if (response.forcePasswordReset && (clientId == null && clientSecret == null)) { - await this.syncService.fullSync(true); return await this.updateTempPassword(); } @@ -383,6 +398,57 @@ export class LoginCommand { return userInput; } + private async migrateToKeyConnector() { + // If no interaction available, alert user to use web vault + if (!this.canInteract) { + await this.logout(); + this.authService.logOut(() => { /* Do nothing */ }); + return Response.error(new MessageResponse('An organization you are a member of is using Key Connector. ' + + 'In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.', null)); + } + + const organization = await this.keyConnectorService.getManagingOrganization(); + + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'list', + name: 'convert', + message: organization.name + ' is using a self-hosted key server. A master password is no longer required to log in for members of this organization. ', + choices: [ + { + name: 'Remove master password and log in', + value: 'remove', + }, + { + name: 'Leave organization and log in', + value: 'leave', + }, + { + name: 'Exit', + value: 'exit', + }, + ], + }); + + if (answer.convert === 'remove') { + await this.keyConnectorService.migrateUser(); + + // Update environment URL - required for api key login + const urls = this.environmentService.getUrls(); + urls.keyConnector = organization.keyConnectorUrl; + await this.environmentService.setUrls(urls, true); + + return await this.handleSuccessResponse(); + } else if (answer.convert === 'leave') { + await this.apiService.postLeaveOrganization(organization.id); + await this.syncService.fullSync(true); + return await this.handleSuccessResponse(); + } else { + await this.logout(); + this.authService.logOut(() => { /* Do nothing */ }); + return Response.error('You have been logged out.'); + } + } + private async apiClientId(): Promise { let clientId: string = null; @@ -432,13 +498,14 @@ export class LoginCommand { }; } - private async getSsoCode(codeChallenge: string, state: string): Promise { + private async openSsoPrompt(codeChallenge: string, state: string): Promise<{ ssoCode: string, orgIdentifier: string }> { return new Promise((resolve, reject) => { const callbackServer = http.createServer((req, res) => { const urlString = 'http://localhost' + req.url; const url = new URL(urlString); const code = url.searchParams.get('code'); const receivedState = url.searchParams.get('state'); + const orgIdentifier = this.getOrgIdentifierFromState(receivedState); res.setHeader('Content-Type', 'text/html'); if (code != null && receivedState != null && this.checkState(receivedState, state)) { res.writeHead(200); @@ -446,7 +513,10 @@ export class LoginCommand { '

Successfully authenticated with the Bitwarden CLI

' + '

You may now close this tab and return to the terminal.

' + ''); - callbackServer.close(() => resolve(code)); + callbackServer.close(() => resolve({ + ssoCode: code, + orgIdentifier: orgIdentifier, + })); } else { res.writeHead(400); res.end('Failed | Bitwarden CLI' + @@ -478,6 +548,15 @@ export class LoginCommand { }); } + private getOrgIdentifierFromState(state: string): string { + if (state === null || state === undefined) { + return null; + } + + const stateSplit = state.split('_identifier='); + return stateSplit.length > 1 ? stateSplit[1] : null; + } + private checkState(state: string, checkState: string): boolean { if (state === null || state === undefined) { return false;