diff --git a/angular/src/components/add-edit-custom-fields.component.ts b/angular/src/components/add-edit-custom-fields.component.ts index e1e4d149..71040ac4 100644 --- a/angular/src/components/add-edit-custom-fields.component.ts +++ b/angular/src/components/add-edit-custom-fields.component.ts @@ -118,7 +118,7 @@ export class AddEditCustomFieldsComponent implements OnChanges { } this.cipher.fields - .filter(f => f.type = FieldType.Linked) + .filter(f => f.type === FieldType.Linked) .forEach(f => f.linkedId = this.linkedFieldOptions[0].value); } } diff --git a/angular/src/components/export.component.ts b/angular/src/components/export.component.ts index 8b9372c3..716f9ef0 100644 --- a/angular/src/components/export.component.ts +++ b/angular/src/components/export.component.ts @@ -69,7 +69,10 @@ export class ExportComponent implements OnInit { } const secret = this.exportForm.get('secret').value; - if (!await this.userVerificationService.verifyUser(secret)) { + try { + await this.userVerificationService.verifyUser(secret); + } catch (e) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), e.message); return; } diff --git a/angular/src/components/verify-master-password.component.ts b/angular/src/components/verify-master-password.component.ts index 01c511c9..9d8bca63 100644 --- a/angular/src/components/verify-master-password.component.ts +++ b/angular/src/components/verify-master-password.component.ts @@ -8,8 +8,8 @@ import { NG_VALUE_ACCESSOR, } from '@angular/forms'; -import { ApiService } from 'jslib-common/abstractions/api.service'; import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; +import { UserVerificationService } from 'jslib-common/abstractions/userVerification.service'; import { VerificationType } from 'jslib-common/enums/verificationType'; @@ -34,27 +34,20 @@ export class VerifyMasterPasswordComponent implements ControlValueAccessor, OnIn private onChange: (value: Verification) => void; - constructor(private keyConnectorService: KeyConnectorService, private apiService: ApiService) { } + constructor(private keyConnectorService: KeyConnectorService, + private userVerificationService: UserVerificationService) { } async ngOnInit() { this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); + this.processChanges(this.secret.value); - this.secret.valueChanges.subscribe(secret => { - if (this.onChange == null) { - return; - } - - this.onChange({ - type: this.usesKeyConnector ? VerificationType.OTP : VerificationType.MasterPassword, - secret: secret, - }); - }); + this.secret.valueChanges.subscribe(secret => this.processChanges(secret)); } async requestOTP() { if (this.usesKeyConnector) { this.disableRequestOTP = true; - await this.apiService.postAccountRequestOTP(); + await this.userVerificationService.requestOTP(); } } @@ -78,4 +71,15 @@ export class VerifyMasterPasswordComponent implements ControlValueAccessor, OnIn this.secret.enable(); } } + + private processChanges(secret: string) { + if (this.onChange == null) { + return; + } + + this.onChange({ + type: this.usesKeyConnector ? VerificationType.OTP : VerificationType.MasterPassword, + secret: secret, + }); + } } diff --git a/common/src/abstractions/userVerification.service.ts b/common/src/abstractions/userVerification.service.ts index 05c7a090..7dcf1d10 100644 --- a/common/src/abstractions/userVerification.service.ts +++ b/common/src/abstractions/userVerification.service.ts @@ -6,4 +6,5 @@ export abstract class UserVerificationService { buildRequest: (verification: Verification, requestClass?: new () => T, alreadyHashed?: boolean) => Promise; verifyUser: (verification: Verification) => Promise; + requestOTP: () => Promise; } diff --git a/common/src/models/export/field.ts b/common/src/models/export/field.ts index 2e98c1bd..0804c123 100644 --- a/common/src/models/export/field.ts +++ b/common/src/models/export/field.ts @@ -1,4 +1,5 @@ import { FieldType } from '../../enums/fieldType'; +import { LinkedIdType } from '../../enums/linkedIdType'; import { FieldView } from '../view/fieldView'; @@ -18,6 +19,7 @@ export class Field { view.type = req.type; view.value = req.value; view.name = req.name; + view.linkedId = req.linkedId; return view; } @@ -25,12 +27,14 @@ export class Field { domain.type = req.type; domain.value = req.value != null ? new EncString(req.value) : null; domain.name = req.name != null ? new EncString(req.name) : null; + domain.linkedId = req.linkedId; return domain; } name: string; value: string; type: FieldType; + linkedId: LinkedIdType; constructor(o?: FieldView | FieldDomain) { if (o == null) { @@ -45,5 +49,6 @@ export class Field { this.value = o.value?.encryptedString; } this.type = o.type; + this.linkedId = o.linkedId; } } diff --git a/common/src/models/response/identityTokenResponse.ts b/common/src/models/response/identityTokenResponse.ts index 7b128f44..c2283956 100644 --- a/common/src/models/response/identityTokenResponse.ts +++ b/common/src/models/response/identityTokenResponse.ts @@ -15,6 +15,7 @@ export class IdentityTokenResponse extends BaseResponse { kdf: KdfType; kdfIterations: number; forcePasswordReset: boolean; + apiUseKeyConnector: boolean; keyConnectorUrl: string; constructor(response: any) { @@ -31,6 +32,7 @@ export class IdentityTokenResponse extends BaseResponse { this.kdf = this.getResponseProperty('Kdf'); this.kdfIterations = this.getResponseProperty('KdfIterations'); this.forcePasswordReset = this.getResponseProperty('ForcePasswordReset'); + this.apiUseKeyConnector = this.getResponseProperty('ApiUseKeyConnector'); this.keyConnectorUrl = this.getResponseProperty('KeyConnectorUrl'); } } diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index bed16d64..55ed6208 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -1609,6 +1609,13 @@ export class ApiService implements ApiServiceAbstraction { authed: boolean, hasResponse: boolean, apiUrl?: string, alterHeaders?: (headers: Headers) => void): Promise { apiUrl = Utils.isNullOrWhitespace(apiUrl) ? this.environmentService.getApiUrl() : apiUrl; + + const requestUrl = apiUrl + path; + // Prevent directory traversal from malicious paths + if (new URL(requestUrl).href !== requestUrl) { + return Promise.reject('Invalid request url path.'); + } + const headers = new Headers({ 'Device-Type': this.deviceType, }); @@ -1647,7 +1654,7 @@ export class ApiService implements ApiServiceAbstraction { } requestInit.headers = headers; - const response = await this.fetch(new Request(apiUrl + path, requestInit)); + const response = await this.fetch(new Request(requestUrl, requestInit)); if (hasResponse && response.status === 200) { const responseJson = await response.json(); diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index e27c77cf..f01778aa 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -376,8 +376,9 @@ export class AuthService implements AuthServiceAbstraction { if (tokenResponse.keyConnectorUrl != null) { await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl); - } else if (this.environmentService.getKeyConnectorUrl() != null) { - await this.keyConnectorService.getAndSetKey(); + } else if (tokenResponse.apiUseKeyConnector) { + const keyConnectorUrl = this.environmentService.getKeyConnectorUrl(); + await this.keyConnectorService.getAndSetKey(keyConnectorUrl); } await this.cryptoService.setEncKey(tokenResponse.key); diff --git a/common/src/services/import.service.ts b/common/src/services/import.service.ts index 3e797031..d04ede50 100644 --- a/common/src/services/import.service.ts +++ b/common/src/services/import.service.ts @@ -85,13 +85,13 @@ export class ImportService implements ImportServiceAbstraction { featuredImportOptions = [ { id: 'bitwardenjson', name: 'Bitwarden (json)' }, { id: 'bitwardencsv', name: 'Bitwarden (csv)' }, - { id: 'lastpasscsv', name: 'LastPass (csv)' }, { id: 'chromecsv', name: 'Chrome (csv)' }, - { id: 'firefoxcsv', name: 'Firefox (csv)' }, - { id: 'safaricsv', name: 'Safari (csv)' }, - { id: 'keepass2xml', name: 'KeePass 2 (xml)' }, - { id: '1password1pif', name: '1Password (1pif)' }, { id: 'dashlanejson', name: 'Dashlane (json)' }, + { id: 'firefoxcsv', name: 'Firefox (csv)' }, + { id: 'keepass2xml', name: 'KeePass 2 (xml)' }, + { id: 'lastpasscsv', name: 'LastPass (csv)' }, + { id: 'safaricsv', name: 'Safari and macOS (csv)' }, + { id: '1password1pif', name: '1Password (1pif)' }, ]; regularImportOptions: ImportOption[] = [ diff --git a/common/src/services/keyConnector.service.ts b/common/src/services/keyConnector.service.ts index 085b87c7..e1f11f4c 100644 --- a/common/src/services/keyConnector.service.ts +++ b/common/src/services/keyConnector.service.ts @@ -1,6 +1,5 @@ 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 { OrganizationService } from '../abstractions/organization.service'; @@ -17,9 +16,8 @@ import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKe export class KeyConnectorService implements KeyConnectorServiceAbstraction { constructor(private stateService: StateService, private cryptoService: CryptoService, - private apiService: ApiService, private environmentService: EnvironmentService, - private tokenService: TokenService, private logService: LogService, - private organizationService: OrganizationService) { } + private apiService: ApiService, private tokenService: TokenService, + private logService: LogService, private organizationService: OrganizationService) { } setUsesKeyConnector(usesKeyConnector: boolean) { return this.stateService.setUsesKeyConnector(usesKeyConnector); @@ -51,15 +49,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { 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.'); - } - + async getAndSetKey(url: string) { try { const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url); const keyArr = Utils.fromB64ToArray(userKeyResponse.key); diff --git a/common/src/services/userVerification.service.ts b/common/src/services/userVerification.service.ts index 7910f44a..2b94ffae 100644 --- a/common/src/services/userVerification.service.ts +++ b/common/src/services/userVerification.service.ts @@ -1,12 +1,8 @@ -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'; @@ -15,17 +11,13 @@ import { SecretVerificationRequest } from '../models/request/secretVerificationR 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) { } + private apiService: ApiService) { } async buildRequest(verification: Verification, requestClass?: new () => T, alreadyHashed?: boolean) { - if (verification?.secret == null || verification.secret === '') { - throw new Error('No secret provided for verification.'); - } + this.validateInput(verification); const request = requestClass != null ? new requestClass() @@ -43,28 +35,35 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } async verifyUser(verification: Verification): Promise { - if (verification?.secret == null || verification.secret === '') { - throw new Error('No secret provided for verification.'); - } + this.validateInput(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; + throw new Error(this.i18nService.t('invalidVerificationCode')); } } 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; + throw new Error(this.i18nService.t('invalidMasterPassword')); } } return true; } + + async requestOTP() { + await this.apiService.postAccountRequestOTP(); + } + + private validateInput(verification: Verification) { + if (verification?.secret == null || verification.secret === '') { + if (verification.type === VerificationType.OTP) { + throw new Error(this.i18nService.t('verificationCodeRequired')); + } else { + throw new Error(this.i18nService.t('masterPassRequired')); + } + } + } }