diff --git a/angular/src/components/add-edit-custom-fields.component.ts b/angular/src/components/add-edit-custom-fields.component.ts index adf47f17..e1e4d149 100644 --- a/angular/src/components/add-edit-custom-fields.component.ts +++ b/angular/src/components/add-edit-custom-fields.component.ts @@ -1,6 +1,8 @@ import { Directive, Input, + OnChanges, + SimpleChanges, } from '@angular/core'; import { @@ -18,13 +20,17 @@ import { CipherType } from 'jslib-common/enums/cipherType'; import { EventType } from 'jslib-common/enums/eventType'; import { FieldType } from 'jslib-common/enums/fieldType'; +import { Utils } from 'jslib-common/misc/utils'; + @Directive() -export class AddEditCustomFieldsComponent { +export class AddEditCustomFieldsComponent implements OnChanges { @Input() cipher: CipherView; + @Input() thisCipherType: CipherType; @Input() editMode: boolean; addFieldType: FieldType = FieldType.Text; addFieldTypeOptions: any[]; + addFieldLinkedTypeOption: any; linkedFieldOptions: any[] = []; cipherType = CipherType; @@ -37,6 +43,17 @@ export class AddEditCustomFieldsComponent { { name: i18nService.t('cfTypeHidden'), value: FieldType.Hidden }, { name: i18nService.t('cfTypeBoolean'), value: FieldType.Boolean }, ]; + this.addFieldLinkedTypeOption = { name: this.i18nService.t('cfTypeLinked'), value: FieldType.Linked }; + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.thisCipherType != null) { + this.setLinkedFieldOptions(); + + if (!changes.thisCipherType.firstChange) { + this.resetCipherLinkedFields(); + } + } } addField() { @@ -48,6 +65,10 @@ export class AddEditCustomFieldsComponent { f.type = this.addFieldType; f.newField = true; + if (f.type === FieldType.Linked) { + f.linkedId = this.linkedFieldOptions[0].value; + } + this.cipher.fields.push(f); } @@ -73,4 +94,31 @@ export class AddEditCustomFieldsComponent { drop(event: CdkDragDrop) { moveItemInArray(this.cipher.fields, event.previousIndex, event.currentIndex); } + + private setLinkedFieldOptions() { + if (this.cipher.linkedFieldOptions == null) { + return; + } + + const options: any = []; + this.cipher.linkedFieldOptions.forEach((linkedFieldOption, id) => + options.push({ name: this.i18nService.t(linkedFieldOption.i18nKey), value: id })); + this.linkedFieldOptions = options.sort(Utils.getSortFunction(this.i18nService, 'name')); + } + + private resetCipherLinkedFields() { + if (this.cipher.fields == null || this.cipher.fields.length === 0) { + return; + } + + // Delete any Linked custom fields if the item type does not support them + if (this.cipher.linkedFieldOptions == null) { + this.cipher.fields = this.cipher.fields.filter(f => f.type !== FieldType.Linked); + return; + } + + this.cipher.fields + .filter(f => f.type = FieldType.Linked) + .forEach(f => f.linkedId = this.linkedFieldOptions[0].value); + } } diff --git a/angular/src/components/add-edit.component.ts b/angular/src/components/add-edit.component.ts index 54cc5d2e..af9be83b 100644 --- a/angular/src/components/add-edit.component.ts +++ b/angular/src/components/add-edit.component.ts @@ -23,6 +23,7 @@ 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 { OrganizationService } from 'jslib-common/abstractions/organization.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 collectionService: CollectionService, protected messagingService: MessagingService, protected eventService: EventService, protected policyService: PolicyService, - private logService: LogService, private organizationService: OrganizationService) { + private logService: LogService, protected passwordRepromptService: PasswordRepromptService, + private organizationService: OrganizationService) { 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 3ea63dee..8b9372c3 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 ec45001a..3e74f067 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'; @@ -14,7 +15,7 @@ import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.serv 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'; @@ -44,7 +45,8 @@ export class LockComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected cryptoService: CryptoService, 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.stateService.accounts.subscribe(async _accounts => { @@ -56,6 +58,11 @@ export class LockComponent implements OnInit { this.biometricText = await this.stateService.getBiometricText(); this.email = await this.stateService.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); @@ -117,7 +124,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 00000000..91c35312 --- /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 4c1d718f..d08559fc 100644 --- a/angular/src/components/sso.component.ts +++ b/angular/src/components/sso.component.ts @@ -57,7 +57,7 @@ export class SsoComponent { await this.stateService.setSsoCodeVerifier(null); await this.stateService.setSsoState(null); 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) { @@ -180,14 +180,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 7bb4fc1b..02f5923d 100644 --- a/angular/src/components/two-factor.component.ts +++ b/angular/src/components/two-factor.component.ts @@ -208,7 +208,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 00000000..ff34926a --- /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 00000000..01c511c9 --- /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 c177a190..942e5b73 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 { StateService } from 'jslib-common/abstractions/state.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; @@ -13,7 +14,8 @@ import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.serv @Injectable() export class AuthGuardService implements CanActivate { constructor(private vaultTimeoutService: VaultTimeoutService, private router: Router, - private messagingService: MessagingService, private stateService: StateService) { } + private messagingService: MessagingService, private keyConnectorService: KeyConnectorService, + private stateService: StateService) { } async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { const isAuthed = await this.stateService.getIsAuthenticated(); @@ -31,6 +33,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 552e558b..03e04531 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 8997c46f..e146a4f5 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 009851be..db335016 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 00000000..a4fbbf5a --- /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 f0e694a0..84ca84f5 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 238a8c9a..22c77f6e 100644 --- a/common/src/abstractions/token.service.ts +++ b/common/src/abstractions/token.service.ts @@ -23,4 +23,5 @@ export abstract class TokenService { getName: () => Promise; getPremium: () => Promise; getIssuer: () => Promise; + getIsExternal: () => boolean; } diff --git a/common/src/abstractions/userVerification.service.ts b/common/src/abstractions/userVerification.service.ts new file mode 100644 index 00000000..05c7a090 --- /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 dabafa81..68fbc6b1 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/fieldType.ts b/common/src/enums/fieldType.ts index c28b26c1..594beba4 100644 --- a/common/src/enums/fieldType.ts +++ b/common/src/enums/fieldType.ts @@ -2,4 +2,5 @@ export enum FieldType { Text = 0, Hidden = 1, Boolean = 2, + Linked = 3, } diff --git a/common/src/enums/linkedIdType.ts b/common/src/enums/linkedIdType.ts new file mode 100644 index 00000000..b2cd2f1c --- /dev/null +++ b/common/src/enums/linkedIdType.ts @@ -0,0 +1,40 @@ +export type LinkedIdType = LoginLinkedId | CardLinkedId | IdentityLinkedId; + +// LoginView +export enum LoginLinkedId { + Username = 100, + Password = 101, +} + +// CardView +export enum CardLinkedId { + CardholderName = 300, + ExpMonth = 301, + ExpYear = 302, + Code = 303, + Brand = 304, + Number = 305, +} + +// IdentityView +export enum IdentityLinkedId { + Title = 400, + MiddleName = 401, + Address1 = 402, + Address2 = 403, + Address3 = 404, + City = 405, + State = 406, + PostalCode = 407, + Country = 408, + Company = 409, + Email = 410, + Phone = 411, + Ssn = 412, + Username = 413, + PassportNumber = 414, + LicenseNumber = 415, + FirstName = 416, + LastName = 417, + FullName = 418, +} diff --git a/common/src/enums/verificationType.ts b/common/src/enums/verificationType.ts new file mode 100644 index 00000000..254da418 --- /dev/null +++ b/common/src/enums/verificationType.ts @@ -0,0 +1,4 @@ +export enum VerificationType { + MasterPassword = 0, + OTP = 1, +} diff --git a/common/src/misc/linkedFieldOption.decorator.ts b/common/src/misc/linkedFieldOption.decorator.ts new file mode 100644 index 00000000..5bf05275 --- /dev/null +++ b/common/src/misc/linkedFieldOption.decorator.ts @@ -0,0 +1,28 @@ +import { ItemView } from '../models/view/itemView'; + +import { LinkedIdType } from '../enums/linkedIdType'; + +export class LinkedMetadata { + constructor(readonly propertyKey: string, private readonly _i18nKey?: string) { } + + get i18nKey() { + return this._i18nKey ?? this.propertyKey; + } +} + +/** + * A decorator used to set metadata used by Linked custom fields. Apply it to a class property or getter to make it + * available as a Linked custom field option. + * @param id - A unique value that is saved in the Field model. It is used to look up the decorated class property. + * @param i18nKey - The i18n key used to describe the decorated class property in the UI. If it is null, then the name + * of the class property will be used as the i18n key. + */ +export function linkedFieldOption(id: LinkedIdType, i18nKey?: string) { + return (prototype: ItemView, propertyKey: string) => { + if (prototype.linkedFieldOptions == null) { + prototype.linkedFieldOptions = new Map(); + } + + prototype.linkedFieldOptions.set(id, new LinkedMetadata(propertyKey, i18nKey)); + }; +} diff --git a/common/src/misc/utils.ts b/common/src/misc/utils.ts index 55c4388e..6ceb459a 100644 --- a/common/src/misc/utils.ts +++ b/common/src/misc/utils.ts @@ -219,7 +219,8 @@ export class Utils { } let httpUrl = uriString.startsWith('http://') || uriString.startsWith('https://'); - if (!httpUrl && uriString.indexOf('://') < 0 && Utils.tldEndingRegex.test(uriString)) { + if (!httpUrl && uriString.indexOf('://') < 0 && Utils.tldEndingRegex.test(uriString) && + uriString.indexOf('@') < 0) { uriString = 'http://' + uriString; httpUrl = true; } diff --git a/common/src/models/api/fieldApi.ts b/common/src/models/api/fieldApi.ts index b7a024cc..b9e8e0e9 100644 --- a/common/src/models/api/fieldApi.ts +++ b/common/src/models/api/fieldApi.ts @@ -1,11 +1,13 @@ import { BaseResponse } from '../response/baseResponse'; import { FieldType } from '../../enums/fieldType'; +import { LinkedIdType } from '../../enums/linkedIdType'; export class FieldApi extends BaseResponse { name: string; value: string; type: FieldType; + linkedId: LinkedIdType; constructor(data: any = null) { super(data); @@ -15,5 +17,6 @@ export class FieldApi extends BaseResponse { this.type = this.getResponseProperty('Type'); this.name = this.getResponseProperty('Name'); this.value = this.getResponseProperty('Value'); + this.linkedId = this.getResponseProperty('linkedId'); } } diff --git a/common/src/models/api/ssoConfigApi.ts b/common/src/models/api/ssoConfigApi.ts index ce7e5b83..9c260116 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/fieldData.ts b/common/src/models/data/fieldData.ts index d28d285b..e83445f2 100644 --- a/common/src/models/data/fieldData.ts +++ b/common/src/models/data/fieldData.ts @@ -1,4 +1,5 @@ import { FieldType } from '../../enums/fieldType'; +import { LinkedIdType } from '../../enums/linkedIdType'; import { FieldApi } from '../api/fieldApi'; @@ -6,6 +7,7 @@ export class FieldData { type: FieldType; name: string; value: string; + linkedId: LinkedIdType; constructor(response?: FieldApi) { if (response == null) { @@ -14,5 +16,6 @@ export class FieldData { this.type = response.type; this.name = response.name; this.value = response.value; + this.linkedId = response.linkedId; } } diff --git a/common/src/models/data/organizationData.ts b/common/src/models/data/organizationData.ts index 3d298a0b..ed44822e 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/field.ts b/common/src/models/domain/field.ts index 898939a2..7174797d 100644 --- a/common/src/models/domain/field.ts +++ b/common/src/models/domain/field.ts @@ -1,4 +1,5 @@ import { FieldType } from '../../enums/fieldType'; +import { LinkedIdType } from '../../enums/linkedIdType'; import { FieldData } from '../data/fieldData'; @@ -12,6 +13,7 @@ export class Field extends Domain { name: EncString; value: EncString; type: FieldType; + linkedId: LinkedIdType; constructor(obj?: FieldData, alreadyEncrypted: boolean = false) { super(); @@ -20,6 +22,7 @@ export class Field extends Domain { } this.type = obj.type; + this.linkedId = obj.linkedId; this.buildDomainModel(this, obj, { name: null, value: null, @@ -39,7 +42,8 @@ export class Field extends Domain { name: null, value: null, type: null, - }, ['type']); + linkedId: null, + }, ['type', 'linkedId']); return f; } } diff --git a/common/src/models/domain/organization.ts b/common/src/models/domain/organization.ts index 8edec4ab..1ad1d6f7 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 7f76af3d..87b580d0 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 00000000..8cc8f985 --- /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/cipherRequest.ts b/common/src/models/request/cipherRequest.ts index 17068c09..23f09462 100644 --- a/common/src/models/request/cipherRequest.ts +++ b/common/src/models/request/cipherRequest.ts @@ -119,6 +119,7 @@ export class CipherRequest { field.type = f.type; field.name = f.name ? f.name.encryptedString : null; field.value = f.value ? f.value.encryptedString : null; + field.linkedId = f.linkedId; return field; }); } diff --git a/common/src/models/request/emailTokenRequest.ts b/common/src/models/request/emailTokenRequest.ts index 3e74669d..90a806b6 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 87363a15..182de57f 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 4d877da6..0d639a09 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 f9a038ac..00000000 --- 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 00000000..554aa1c6 --- /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 ced9288f..6eeb0185 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 23a47f9e..c9138597 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 ddd7c59a..3352f327 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 2dd6a96a..fa0a96c0 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 1777f39a..9465e2f9 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 dabeb7d9..ce2fedb5 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 943d9e41..8fa5fcd0 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 ef9308eb..09976c31 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 4ad0696f..29a9dd81 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 aa2fe8fd..7b128f44 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 0397388e..4da60be3 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 b70d9567..d4c77792 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 d9382da3..dcf24561 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 1c6e64fa..913ff820 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/models/view/cardView.ts b/common/src/models/view/cardView.ts index db874079..94e7ae49 100644 --- a/common/src/models/view/cardView.ts +++ b/common/src/models/view/cardView.ts @@ -1,11 +1,19 @@ -import { View } from './view'; +import { ItemView } from './itemView'; import { Card } from '../domain/card'; -export class CardView implements View { +import { CardLinkedId as LinkedId } from '../../enums/linkedIdType'; + +import { linkedFieldOption } from '../../misc/linkedFieldOption.decorator'; + +export class CardView extends ItemView { + @linkedFieldOption(LinkedId.CardholderName) cardholderName: string = null; + @linkedFieldOption(LinkedId.ExpMonth, 'expirationMonth') expMonth: string = null; + @linkedFieldOption(LinkedId.ExpYear, 'expirationYear') expYear: string = null; + @linkedFieldOption(LinkedId.Code, 'securityCode') code: string = null; // tslint:disable @@ -15,7 +23,7 @@ export class CardView implements View { // tslint:enable constructor(c?: Card) { - // ctor + super(); } get maskedCode(): string { @@ -26,6 +34,7 @@ export class CardView implements View { return this.number != null ? '•'.repeat(this.number.length) : null; } + @linkedFieldOption(LinkedId.Brand) get brand(): string { return this._brand; } @@ -34,6 +43,7 @@ export class CardView implements View { this._subTitle = null; } + @linkedFieldOption(LinkedId.Number) get number(): string { return this._number; } diff --git a/common/src/models/view/cipherView.ts b/common/src/models/view/cipherView.ts index 9a2208c0..e4e5c3f1 100644 --- a/common/src/models/view/cipherView.ts +++ b/common/src/models/view/cipherView.ts @@ -1,5 +1,6 @@ import { CipherRepromptType } from '../../enums/cipherRepromptType'; import { CipherType } from '../../enums/cipherType'; +import { LinkedIdType } from '../../enums/linkedIdType'; import { Cipher } from '../domain/cipher'; @@ -7,6 +8,7 @@ import { AttachmentView } from './attachmentView'; import { CardView } from './cardView'; import { FieldView } from './fieldView'; import { IdentityView } from './identityView'; +import { ItemView } from './itemView'; import { LoginView } from './loginView'; import { PasswordHistoryView } from './passwordHistoryView'; import { SecureNoteView } from './secureNoteView'; @@ -57,16 +59,16 @@ export class CipherView implements View { this.reprompt = c.reprompt ?? CipherRepromptType.None; } - get subTitle(): string { + private get item() { switch (this.type) { case CipherType.Login: - return this.login.subTitle; + return this.login; case CipherType.SecureNote: - return this.secureNote.subTitle; + return this.secureNote; case CipherType.Card: - return this.card.subTitle; + return this.card; case CipherType.Identity: - return this.identity.subTitle; + return this.identity; default: break; } @@ -74,6 +76,10 @@ export class CipherView implements View { return null; } + get subTitle(): string { + return this.item.subTitle; + } + get hasPasswordHistory(): boolean { return this.passwordHistory && this.passwordHistory.length > 0; } @@ -109,4 +115,22 @@ export class CipherView implements View { get isDeleted(): boolean { return this.deletedDate != null; } + + get linkedFieldOptions() { + return this.item.linkedFieldOptions; + } + + linkedFieldValue(id: LinkedIdType) { + const linkedFieldOption = this.linkedFieldOptions?.get(id); + if (linkedFieldOption == null) { + return null; + } + + const item = this.item; + return this.item[linkedFieldOption.propertyKey as keyof typeof item]; + } + + linkedFieldI18nKey(id: LinkedIdType): string { + return this.linkedFieldOptions.get(id)?.i18nKey; + } } diff --git a/common/src/models/view/fieldView.ts b/common/src/models/view/fieldView.ts index 66c36744..3202d4b1 100644 --- a/common/src/models/view/fieldView.ts +++ b/common/src/models/view/fieldView.ts @@ -1,4 +1,5 @@ import { FieldType } from '../../enums/fieldType'; +import { LinkedIdType } from '../../enums/linkedIdType'; import { View } from './view'; @@ -10,6 +11,7 @@ export class FieldView implements View { type: FieldType = null; newField: boolean = false; // Marks if the field is new and hasn't been saved showValue: boolean = false; + linkedId: LinkedIdType = null; constructor(f?: Field) { if (!f) { @@ -17,6 +19,7 @@ export class FieldView implements View { } this.type = f.type; + this.linkedId = f.linkedId; } get maskedValue(): string { diff --git a/common/src/models/view/identityView.ts b/common/src/models/view/identityView.ts index 487a59bf..844a8acd 100644 --- a/common/src/models/view/identityView.ts +++ b/common/src/models/view/identityView.ts @@ -1,25 +1,45 @@ -import { View } from './view'; +import { ItemView } from './itemView'; import { Identity } from '../domain/identity'; import { Utils } from '../../misc/utils'; -export class IdentityView implements View { +import { IdentityLinkedId as LinkedId } from '../../enums/linkedIdType'; + +import { linkedFieldOption } from '../../misc/linkedFieldOption.decorator'; + +export class IdentityView extends ItemView { + @linkedFieldOption(LinkedId.Title) title: string = null; + @linkedFieldOption(LinkedId.MiddleName) middleName: string = null; + @linkedFieldOption(LinkedId.Address1) address1: string = null; + @linkedFieldOption(LinkedId.Address2) address2: string = null; + @linkedFieldOption(LinkedId.Address3) address3: string = null; + @linkedFieldOption(LinkedId.City, 'cityTown') city: string = null; + @linkedFieldOption(LinkedId.State, 'stateProvince') state: string = null; + @linkedFieldOption(LinkedId.PostalCode, 'zipPostalCode') postalCode: string = null; + @linkedFieldOption(LinkedId.Country) country: string = null; + @linkedFieldOption(LinkedId.Company) company: string = null; + @linkedFieldOption(LinkedId.Email) email: string = null; + @linkedFieldOption(LinkedId.Phone) phone: string = null; + @linkedFieldOption(LinkedId.Ssn) ssn: string = null; + @linkedFieldOption(LinkedId.Username) username: string = null; + @linkedFieldOption(LinkedId.PassportNumber) passportNumber: string = null; + @linkedFieldOption(LinkedId.LicenseNumber) licenseNumber: string = null; // tslint:disable @@ -29,9 +49,10 @@ export class IdentityView implements View { // tslint:enable constructor(i?: Identity) { - // ctor + super(); } + @linkedFieldOption(LinkedId.FirstName) get firstName(): string { return this._firstName; } @@ -40,6 +61,7 @@ export class IdentityView implements View { this._subTitle = null; } + @linkedFieldOption(LinkedId.LastName) get lastName(): string { return this._lastName; } @@ -65,6 +87,7 @@ export class IdentityView implements View { return this._subTitle; } + @linkedFieldOption(LinkedId.FullName) get fullName(): string { if (this.title != null || this.firstName != null || this.middleName != null || this.lastName != null) { let name = ''; diff --git a/common/src/models/view/itemView.ts b/common/src/models/view/itemView.ts new file mode 100644 index 00000000..aaeac473 --- /dev/null +++ b/common/src/models/view/itemView.ts @@ -0,0 +1,8 @@ +import { View } from './view'; + +import { LinkedMetadata } from '../../misc/linkedFieldOption.decorator'; + +export abstract class ItemView implements View { + linkedFieldOptions: Map; + abstract get subTitle(): string; +} diff --git a/common/src/models/view/loginView.ts b/common/src/models/view/loginView.ts index 7db24310..3bd1ea58 100644 --- a/common/src/models/view/loginView.ts +++ b/common/src/models/view/loginView.ts @@ -1,18 +1,27 @@ +import { ItemView } from './itemView'; import { LoginUriView } from './loginUriView'; -import { View } from './view'; import { Utils } from '../../misc/utils'; + import { Login } from '../domain/login'; -export class LoginView implements View { +import { LoginLinkedId as LinkedId } from '../../enums/linkedIdType'; + +import { linkedFieldOption } from '../../misc/linkedFieldOption.decorator'; + +export class LoginView extends ItemView { + @linkedFieldOption(LinkedId.Username) username: string = null; + @linkedFieldOption(LinkedId.Password) password: string = null; + passwordRevisionDate?: Date = null; totp: string = null; uris: LoginUriView[] = null; autofillOnPageLoad: boolean = null; constructor(l?: Login) { + super(); if (!l) { return; } diff --git a/common/src/models/view/secureNoteView.ts b/common/src/models/view/secureNoteView.ts index 6bd4cde7..2d209025 100644 --- a/common/src/models/view/secureNoteView.ts +++ b/common/src/models/view/secureNoteView.ts @@ -1,13 +1,14 @@ import { SecureNoteType } from '../../enums/secureNoteType'; -import { View } from './view'; +import { ItemView } from './itemView'; import { SecureNote } from '../domain/secureNote'; -export class SecureNoteView implements View { +export class SecureNoteView extends ItemView { type: SecureNoteType = null; constructor(n?: SecureNote) { + super(); if (!n) { return; } diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index acf9419c..bed16d64 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 743e1d1d..96530c44 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -6,9 +6,9 @@ import { Account } from '../models/domain/account'; 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'; @@ -21,7 +21,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'; @@ -102,6 +104,7 @@ export class AuthService implements AuthServiceAbstraction { private i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, private vaultTimeoutService: VaultTimeoutService, private logService: LogService, protected cryptoFunctionService: CryptoFunctionService, + private keyConnectorService: KeyConnectorService, protected stateService: StateService, private setCryptoKeys = true) { } @@ -371,16 +374,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); @@ -397,11 +394,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), await 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); @@ -410,16 +407,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/cipher.service.ts b/common/src/services/cipher.service.ts index e0ace39c..9ee245dd 100644 --- a/common/src/services/cipher.service.ts +++ b/common/src/services/cipher.service.ts @@ -212,6 +212,7 @@ export class CipherService implements CipherServiceAbstraction { async encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise { const field = new Field(); field.type = fieldModel.type; + field.linkedId = fieldModel.linkedId; // normalize boolean type field values if (fieldModel.type === FieldType.Boolean && fieldModel.value !== 'true') { fieldModel.value = 'false'; diff --git a/common/src/services/environment.service.ts b/common/src/services/environment.service.ts index f1862dbb..0cbe1a7d 100644 --- a/common/src/services/environment.service.ts +++ b/common/src/services/environment.service.ts @@ -17,6 +17,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { private iconsUrl: string; private notificationsUrl: string; private eventsUrl: string; + private keyConnectorUrl: string; constructor(private stateService: StateService) {} @@ -101,6 +102,10 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { return 'https://events.bitwarden.com'; } + getKeyConnectorUrl() { + return this.keyConnectorUrl; + } + async setUrlsFromStorage(): Promise { const urlsObj: any = await this.stateService.getEnvironmentUrls(); const urls = urlsObj || { @@ -111,6 +116,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { notifications: null, events: null, webVault: null, + keyConnector: null, }; const envUrls = new EnvironmentUrls(); @@ -126,6 +132,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 { @@ -136,6 +143,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.stateService.setEnvironmentUrls({ @@ -146,6 +154,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { icons: urls.icons, notifications: urls.notifications, events: urls.events, + keyConnector: urls.keyConnector, }); } @@ -156,6 +165,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); @@ -171,6 +181,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 00000000..68aac9a2 --- /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 945980be..b0d6e36d 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 { OrganizationService } from '../abstractions/organization.service'; @@ -12,6 +13,11 @@ import { SendService } from '../abstractions/send.service'; import { SettingsService } from '../abstractions/settings.service'; import { StateService } from '../abstractions/state.service'; import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; +<<<<<<< HEAD +======= +import { TokenService } from '../abstractions/token.service'; +import { UserService } from '../abstractions/user.service'; +>>>>>>> master import { CipherData } from '../models/data/cipherData'; import { CollectionData } from '../models/data/collectionData'; @@ -42,8 +48,13 @@ export class SyncService implements SyncServiceAbstraction { private cryptoService: CryptoService, private collectionService: CollectionService, private messagingService: MessagingService, private policyService: PolicyService, private sendService: SendService, private logService: LogService, +<<<<<<< HEAD private logoutCallback: (expired: boolean) => Promise, private stateService: StateService, private organizationService: OrganizationService, private providerService: ProviderService) { +======= + private tokenService: TokenService, private keyConnectorService: KeyConnectorService, + private logoutCallback: (expired: boolean) => Promise) { +>>>>>>> master } async getLastSync(): Promise { @@ -287,9 +298,16 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setEncPrivateKey(response.privateKey); await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); +<<<<<<< HEAD await this.stateService.setSecurityStamp(response.securityStamp); await this.stateService.setEmailVerified(response.emailVerified); await this.stateService.setForcePasswordReset(response.forcePasswordReset); +======= + await this.userService.setSecurityStamp(response.securityStamp); + await this.userService.setEmailVerified(response.emailVerified); + await this.userService.setForcePasswordReset(response.forcePasswordReset); + await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); +>>>>>>> master const organizations: { [id: string]: OrganizationData; } = {}; response.organizations.forEach(o => { @@ -307,6 +325,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.organizationService.save(organizations), this.providerService.save(providers), diff --git a/common/src/services/token.service.ts b/common/src/services/token.service.ts index 9336459a..e1388af9 100644 --- a/common/src/services/token.service.ts +++ b/common/src/services/token.service.ts @@ -209,6 +209,27 @@ export class TokenService implements TokenServiceAbstraction { return decoded.iss as string; } +<<<<<<< HEAD +======= + 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 + return; + } + + return this.storageService.save(key, value); + } + +>>>>>>> master private async skipTokenStorage(): Promise { const timeout = await this.stateService.getVaultTimeout(); const action = await this.stateService.getVaultTimeoutAction(); diff --git a/common/src/services/user.service.ts b/common/src/services/user.service.ts new file mode 100644 index 00000000..658179f9 --- /dev/null +++ b/common/src/services/user.service.ts @@ -0,0 +1,233 @@ +import { StorageService } from '../abstractions/storage.service'; +import { TokenService } from '../abstractions/token.service'; +import { UserService as UserServiceAbstraction } from '../abstractions/user.service'; + +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'; + +const Keys = { + userId: 'userId', + userEmail: 'userEmail', + stamp: 'securityStamp', + kdf: 'kdf', + kdfIterations: 'kdfIterations', + organizationsPrefix: 'organizations_', + providersPrefix: 'providers_', + emailVerified: 'emailVerified', + forcePasswordReset: 'forcePasswordReset', +}; + +export class UserService implements UserServiceAbstraction { + private userId: string; + private email: string; + private stamp: string; + private kdf: KdfType; + private kdfIterations: number; + private emailVerified: boolean; + private forcePasswordReset: boolean; + + constructor(private tokenService: TokenService, private storageService: StorageService) { } + + async setInformation(userId: string, email: string, kdf: KdfType, kdfIterations: number): Promise { + this.email = email; + this.userId = userId; + this.kdf = kdf; + this.kdfIterations = kdfIterations; + + await this.storageService.save(Keys.userEmail, email); + await this.storageService.save(Keys.userId, userId); + await this.storageService.save(Keys.kdf, kdf); + await this.storageService.save(Keys.kdfIterations, kdfIterations); + } + + setSecurityStamp(stamp: string): Promise { + this.stamp = stamp; + return this.storageService.save(Keys.stamp, stamp); + } + + setEmailVerified(emailVerified: boolean) { + this.emailVerified = emailVerified; + return this.storageService.save(Keys.emailVerified, emailVerified); + } + + setForcePasswordReset(forcePasswordReset: boolean) { + this.forcePasswordReset = forcePasswordReset; + return this.storageService.save(Keys.forcePasswordReset, forcePasswordReset); + } + + async getUserId(): Promise { + if (this.userId == null) { + this.userId = await this.storageService.get(Keys.userId); + } + return this.userId; + } + + async getEmail(): Promise { + if (this.email == null) { + this.email = await this.storageService.get(Keys.userEmail); + } + return this.email; + } + + async getSecurityStamp(): Promise { + if (this.stamp == null) { + this.stamp = await this.storageService.get(Keys.stamp); + } + return this.stamp; + } + + async getKdf(): Promise { + if (this.kdf == null) { + this.kdf = await this.storageService.get(Keys.kdf); + } + return this.kdf; + } + + async getKdfIterations(): Promise { + if (this.kdfIterations == null) { + this.kdfIterations = await this.storageService.get(Keys.kdfIterations); + } + return this.kdfIterations; + } + + async getEmailVerified(): Promise { + if (this.emailVerified == null) { + this.emailVerified = await this.storageService.get(Keys.emailVerified); + } + return this.emailVerified; + } + + async getForcePasswordReset(): Promise { + if (this.forcePasswordReset == null) { + this.forcePasswordReset = await this.storageService.get(Keys.forcePasswordReset); + } + return this.forcePasswordReset; + } + + async clear(): Promise { + const userId = await this.getUserId(); + + await this.storageService.remove(Keys.userId); + await this.storageService.remove(Keys.userEmail); + await this.storageService.remove(Keys.stamp); + await this.storageService.remove(Keys.kdf); + await this.storageService.remove(Keys.kdfIterations); + await this.storageService.remove(Keys.forcePasswordReset); + await this.clearOrganizations(userId); + await this.clearProviders(userId); + + this.userId = this.email = this.stamp = null; + this.kdf = null; + this.kdfIterations = null; + } + + async isAuthenticated(): Promise { + const token = await this.tokenService.getToken(); + if (token == null) { + return false; + } + + const userId = await this.getUserId(); + return userId != null; + } + + async canAccessPremium(): Promise { + const authed = await this.isAuthenticated(); + if (!authed) { + return false; + } + + const tokenPremium = this.tokenService.getPremium(); + if (tokenPremium) { + return true; + } + + const orgs = await this.getAllOrganizations(); + for (let i = 0; i < orgs.length; i++) { + if (orgs[i].usersGetPremium && orgs[i].enabled) { + return true; + } + } + return false; + } + + async getOrganization(id: string): Promise { + const userId = await this.getUserId(); + const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( + Keys.organizationsPrefix + userId); + if (organizations == null || !organizations.hasOwnProperty(id)) { + return null; + } + + return new Organization(organizations[id]); + } + + async getOrganizationByIdentifier(identifier: string): Promise { + const organizations = await this.getAllOrganizations(); + if (organizations == null || organizations.length === 0) { + return null; + } + + return organizations.find(o => o.identifier === identifier); + } + + async getAllOrganizations(): Promise { + const userId = await this.getUserId(); + const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( + Keys.organizationsPrefix + userId); + const response: Organization[] = []; + for (const id in organizations) { + if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) { + response.push(new Organization(organizations[id])); + } + } + return response; + } + + async replaceOrganizations(organizations: { [id: string]: OrganizationData; }): Promise { + const userId = await this.getUserId(); + await this.storageService.save(Keys.organizationsPrefix + userId, organizations); + } + + async clearOrganizations(userId: string): Promise { + await this.storageService.remove(Keys.organizationsPrefix + userId); + } + + async getProvider(id: string): Promise { + const userId = await this.getUserId(); + const providers = await this.storageService.get<{ [id: string]: ProviderData; }>( + Keys.providersPrefix + userId); + if (providers == null || !providers.hasOwnProperty(id)) { + return null; + } + + return new Provider(providers[id]); + } + + async getAllProviders(): Promise { + const userId = await this.getUserId(); + const providers = await this.storageService.get<{ [id: string]: ProviderData; }>( + Keys.providersPrefix + userId); + const response: Provider[] = []; + for (const id in providers) { + if (providers.hasOwnProperty(id)) { + response.push(new Provider(providers[id])); + } + } + return response; + } + + async replaceProviders(providers: { [id: string]: ProviderData; }): Promise { + const userId = await this.getUserId(); + await this.storageService.save(Keys.providersPrefix + userId, providers); + } + + async clearProviders(userId: string): Promise { + await this.storageService.remove(Keys.providersPrefix + userId); + } +} diff --git a/common/src/services/userVerification.service.ts b/common/src/services/userVerification.service.ts new file mode 100644 index 00000000..7910f44a --- /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 bf6d7246..fd7d1b4e 100644 --- a/common/src/services/vaultTimeout.service.ts +++ b/common/src/services/vaultTimeout.service.ts @@ -2,6 +2,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'; @@ -18,10 +19,18 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private cryptoService: CryptoService, +<<<<<<< HEAD protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, private searchService: SearchService, private tokenService: TokenService, private policyService: PolicyService, private stateService: StateService, private lockedCallback: () => Promise = null, private loggedOutCallback: (userId?: string) => Promise = null) { +======= + 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) { +>>>>>>> master } init(checkOnInterval: boolean) { @@ -94,6 +103,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } +<<<<<<< HEAD if (userId == null || userId === await this.stateService.getUserId()) { this.searchService.clearIndex(); } @@ -109,6 +119,29 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.stateService.setBiometricLocked(true, { userId: userId }); this.messagingService.send('locked', { userId: userId }); +======= + 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); + await this.cryptoService.clearOrgKeys(true); + await this.cryptoService.clearKeyPair(true); + await this.cryptoService.clearEncKey(true); + + this.folderService.clearCache(); + this.cipherService.clearCache(); + this.collectionService.clearCache(); + this.searchService.clearIndex(); + this.messagingService.send('locked'); +>>>>>>> master if (this.lockedCallback != null) { await this.lockedCallback(); } diff --git a/common/src/types/verification.ts b/common/src/types/verification.ts new file mode 100644 index 00000000..72ee8b0a --- /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/electron/package-lock.json b/electron/package-lock.json index 00ac965a..1201fd87 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@bitwarden/jslib-common": "file:../common", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", - "electron": "14.0.1", + "electron": "14.2.0", "electron-log": "4.4.1", "electron-store": "8.0.1", "electron-updater": "4.3.9", @@ -25,6 +25,7 @@ } }, "../common": { + "name": "@bitwarden/jslib-common", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -555,10 +556,11 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, "node_modules/electron": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-14.0.1.tgz", - "integrity": "sha512-1XILvfE5mQEBz5L/QeNfcwC3PxAIjwMyA3GR8Naw5C0IKAnHl3lAdjczbtGX8nqbcEpOAVo+4TMSpcPD3zxe8Q==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-14.2.0.tgz", + "integrity": "sha512-6CmAv1P0xcwK3FQOSA27fHI36/wctSFVgj46VODn56srXXQWeolkK1VzeAFNE613iAuuH9jJdHvE3gz+c7XkNA==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "@electron/get": "^1.0.1", "@types/node": "^14.6.2", @@ -2286,9 +2288,9 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, "electron": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-14.0.1.tgz", - "integrity": "sha512-1XILvfE5mQEBz5L/QeNfcwC3PxAIjwMyA3GR8Naw5C0IKAnHl3lAdjczbtGX8nqbcEpOAVo+4TMSpcPD3zxe8Q==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-14.2.0.tgz", + "integrity": "sha512-6CmAv1P0xcwK3FQOSA27fHI36/wctSFVgj46VODn56srXXQWeolkK1VzeAFNE613iAuuH9jJdHvE3gz+c7XkNA==", "requires": { "@electron/get": "^1.0.1", "@types/node": "^14.6.2", diff --git a/electron/package.json b/electron/package.json index 733f0daf..ffd2c2f7 100644 --- a/electron/package.json +++ b/electron/package.json @@ -27,7 +27,7 @@ "dependencies": { "@bitwarden/jslib-common": "file:../common", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", - "electron": "14.0.1", + "electron": "14.2.0", "electron-log": "4.4.1", "electron-store": "8.0.1", "electron-updater": "4.3.9", diff --git a/node/src/cli/commands/login.command.ts b/node/src/cli/commands/login.command.ts index 6487092a..df3ca05e 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 { SyncService } from 'jslib-common/abstractions/sync.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'; @@ -47,8 +49,14 @@ export class LoginCommand { protected i18nService: I18nService, protected environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected platformUtilsService: PlatformUtilsService, +<<<<<<< HEAD protected stateService: StateService, protected cryptoService: CryptoService, protected policyService: PolicyService, clientId: string, private syncService: SyncService) { +======= + protected userService: UserService, protected cryptoService: CryptoService, + protected policyService: PolicyService, clientId: string, private syncService: SyncService, + protected keyConnectorService: KeyConnectorService) { +>>>>>>> master this.clientId = clientId; } @@ -57,6 +65,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 +88,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 +162,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 +232,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 +267,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 +403,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 +503,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 +518,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 +553,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; diff --git a/package-lock.json b/package-lock.json index c3988504..b3e72c7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ } }, "common": { + "name": "@bitwarden/jslib-common", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -100,12 +101,13 @@ } }, "electron": { + "name": "@bitwarden/jslib-electron", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/jslib-common": "file:../common", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", - "electron": "14.0.1", + "electron": "14.2.0", "electron-log": "4.4.1", "electron-store": "8.0.1", "electron-updater": "4.3.9", @@ -119,6 +121,7 @@ } }, "node": { + "name": "@bitwarden/jslib-node", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -2791,9 +2794,9 @@ "dev": true }, "node_modules/electron": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-14.0.1.tgz", - "integrity": "sha512-1XILvfE5mQEBz5L/QeNfcwC3PxAIjwMyA3GR8Naw5C0IKAnHl3lAdjczbtGX8nqbcEpOAVo+4TMSpcPD3zxe8Q==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-14.2.0.tgz", + "integrity": "sha512-6CmAv1P0xcwK3FQOSA27fHI36/wctSFVgj46VODn56srXXQWeolkK1VzeAFNE613iAuuH9jJdHvE3gz+c7XkNA==", "hasInstallScript": true, "dependencies": { "@electron/get": "^1.0.1", @@ -9832,7 +9835,7 @@ "@bitwarden/jslib-common": "file:../common", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", "@types/node": "^14.17.1", - "electron": "14.0.1", + "electron": "14.2.0", "electron-log": "4.4.1", "electron-store": "8.0.1", "electron-updater": "4.3.9", @@ -11854,9 +11857,9 @@ "dev": true }, "electron": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-14.0.1.tgz", - "integrity": "sha512-1XILvfE5mQEBz5L/QeNfcwC3PxAIjwMyA3GR8Naw5C0IKAnHl3lAdjczbtGX8nqbcEpOAVo+4TMSpcPD3zxe8Q==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-14.2.0.tgz", + "integrity": "sha512-6CmAv1P0xcwK3FQOSA27fHI36/wctSFVgj46VODn56srXXQWeolkK1VzeAFNE613iAuuH9jJdHvE3gz+c7XkNA==", "requires": { "@electron/get": "^1.0.1", "@types/node": "^14.6.2",