1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-12-05 09:14:28 +01:00

Add support for requesting and using otp for verifying some requests (#527)

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
Oscar Hinton 2021-11-09 17:01:22 +01:00 committed by GitHub
parent 99ff3feb53
commit 8f177e2d3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 746 additions and 223 deletions

View File

@ -22,6 +22,7 @@ import { FolderService } from 'jslib-common/abstractions/folder.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
@ -80,6 +81,7 @@ export class AddEditComponent implements OnInit {
currentDate = new Date();
allowPersonal = true;
reprompt: boolean = false;
canUseReprompt: boolean = true;
protected writeableCollections: CollectionView[];
private previousCipherId: string;
@ -89,7 +91,8 @@ export class AddEditComponent implements OnInit {
protected auditService: AuditService, protected stateService: StateService,
protected userService: UserService, protected collectionService: CollectionService,
protected messagingService: MessagingService, protected eventService: EventService,
protected policyService: PolicyService, private logService: LogService) {
protected policyService: PolicyService, protected passwordRepromptService: PasswordRepromptService,
private logService: LogService) {
this.typeOptions = [
{ name: i18nService.t('typeLogin'), value: CipherType.Login },
{ name: i18nService.t('typeCard'), value: CipherType.Card },
@ -169,6 +172,8 @@ export class AddEditComponent implements OnInit {
}
this.writeableCollections = await this.loadCollections();
this.canUseReprompt = await this.passwordRepromptService.enabled();
}
async load() {

View File

@ -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<string>;
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);

View File

@ -5,6 +5,7 @@ import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
@ -18,7 +19,7 @@ import { ConstantsService } from 'jslib-common/services/constants.service';
import { EncString } from 'jslib-common/models/domain/encString';
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
import { PasswordVerificationRequest } from 'jslib-common/models/request/passwordVerificationRequest';
import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest';
import { Utils } from 'jslib-common/misc/utils';
@ -48,7 +49,8 @@ export class LockComponent implements OnInit {
protected userService: UserService, protected cryptoService: CryptoService,
protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService,
protected environmentService: EnvironmentService, protected stateService: StateService,
protected apiService: ApiService, private logService: LogService) { }
protected apiService: ApiService, private logService: LogService,
private keyConnectorService: KeyConnectorService) { }
async ngOnInit() {
this.pinSet = await this.vaultTimeoutService.isPinLockSet();
@ -59,6 +61,11 @@ export class LockComponent implements OnInit {
this.biometricText = await this.storageService.get(ConstantsService.biometricText);
this.email = await this.userService.getEmail();
// Users with key connector and without biometric or pin has no MP to unlock using
if (await this.keyConnectorService.getUsesKeyConnector() && !(this.biometricLock || this.pinLock)) {
await this.vaultTimeoutService.logOut();
}
const webVaultUrl = this.environmentService.getWebVaultUrl();
const vaultUrl = webVaultUrl === 'https://vault.bitwarden.com' ? 'https://bitwarden.com' : webVaultUrl;
this.webVaultHostname = Utils.getHostname(vaultUrl);
@ -119,7 +126,7 @@ export class LockComponent implements OnInit {
if (storedKeyHash != null) {
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, key);
} else {
const request = new PasswordVerificationRequest();
const request = new SecretVerificationRequest();
const serverKeyHash = await this.cryptoService.hashPassword(this.masterPassword, key,
HashPurpose.ServerAuthorization);
request.masterPasswordHash = serverKeyHash;

View File

@ -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<any>;
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);
}
}
}

View File

@ -60,7 +60,7 @@ export class SsoComponent {
await this.storageService.remove(ConstantsService.ssoCodeVerifierKey);
await this.storageService.remove(ConstantsService.ssoStateKey);
if (qParams.code != null && codeVerifier != null && state != null && this.checkState(state, qParams.state)) {
await this.logIn(qParams.code, codeVerifier, this.getOrgIdentiferFromState(qParams.state));
await this.logIn(qParams.code, codeVerifier, this.getOrgIdentifierFromState(qParams.state));
}
} else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null &&
qParams.codeChallenge != null) {
@ -183,14 +183,14 @@ export class SsoComponent {
}
} catch (e) {
this.logService.error(e);
if (e.message === 'Unable to reach crypto agent') {
this.platformUtilsService.showToast('error', null, this.i18nService.t('ssoCryptoAgentUnavailable'));
if (e.message === 'Unable to reach key connector') {
this.platformUtilsService.showToast('error', null, this.i18nService.t('ssoKeyConnectorUnavailable'));
}
}
this.loggingIn = false;
}
private getOrgIdentiferFromState(state: string): string {
private getOrgIdentifierFromState(state: string): string {
if (state === null || state === undefined) {
return null;
}

View File

@ -211,7 +211,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
}
try {
const request = new TwoFactorEmailRequest(this.authService.email, this.authService.masterPasswordHash);
const request = new TwoFactorEmailRequest();
request.email = this.authService.email;
request.masterPasswordHash = this.authService.masterPasswordHash;
this.emailPromise = this.apiService.postTwoFactorEmail(request);
await this.emailPromise;
if (doToast) {

View File

@ -0,0 +1,18 @@
<ng-container *ngIf="!usesKeyConnector">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
[formControl]="secret" required appAutofocus appInputVerbatim>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<div class="form-group">
<button type="button" class="btn btn-primary" (click)="requestOTP()" [disabled]="disableRequestOTP">
{{'requestVerificationCode' | i18n}}
</button>
</div>
<div class="form-group">
<label for="verificationCode">{{'verificationCode' | i18n}}</label>
<input id="verificationCode" type="input" name="verificationCode" class="form-control"
[formControl]="secret" required appAutofocus appInputVerbatim>
</div>
</ng-container>

View File

@ -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();
}
}
}

View File

@ -6,6 +6,7 @@ import {
RouterStateSnapshot,
} from '@angular/router';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
@ -13,7 +14,7 @@ import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.serv
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService,
private router: Router, private messagingService: MessagingService) { }
private router: Router, private messagingService: MessagingService, private keyConnectorService: KeyConnectorService) { }
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
const isAuthed = await this.userService.isAuthenticated();
@ -31,6 +32,11 @@ export class AuthGuardService implements CanActivate {
return false;
}
if (!routerState.url.includes('remove-password') && await this.keyConnectorService.getConvertAccountRequired()) {
this.router.navigate(['/remove-password']);
return false;
}
return true;
}
}

View File

@ -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();
}
}

View File

@ -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<any>;
postPassword: (request: PasswordRequest) => Promise<any>;
setPassword: (request: SetPasswordRequest) => Promise<any>;
postSetCryptoAgentKey: (request: SetCryptoAgentKeyRequest) => Promise<any>;
postSecurityStamp: (request: PasswordVerificationRequest) => Promise<any>;
deleteAccount: (request: PasswordVerificationRequest) => Promise<any>;
postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise<any>;
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
deleteAccount: (request: SecretVerificationRequest) => Promise<any>;
getAccountRevisionDate: () => Promise<number>;
postPasswordHint: (request: PasswordHintRequest) => Promise<any>;
postRegister: (request: RegisterRequest) => Promise<any>;
@ -192,13 +193,16 @@ export abstract class ApiService {
postAccountKeys: (request: KeysRequest) => Promise<any>;
postAccountVerifyEmail: () => Promise<any>;
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;
postAccountVerifyPassword: (request: PasswordVerificationRequest) => Promise<any>;
postAccountVerifyPassword: (request: SecretVerificationRequest) => Promise<any>;
postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise<any>;
postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise<any>;
postAccountKdf: (request: KdfRequest) => Promise<any>;
postUserApiKey: (id: string, request: PasswordVerificationRequest) => Promise<ApiKeyResponse>;
postUserRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise<ApiKeyResponse>;
postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
postAccountRequestOTP: () => Promise<void>;
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
postConvertToKeyConnector: () => Promise<void>;
getFolder: (id: string) => Promise<FolderResponse>;
postFolder: (request: FolderRequest) => Promise<FolderResponse>;
@ -240,7 +244,7 @@ export abstract class ApiService {
putShareCiphers: (request: CipherBulkShareRequest) => Promise<any>;
putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise<any>;
putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise<any>;
postPurgeCiphers: (request: PasswordVerificationRequest, organizationId?: string) => Promise<any>;
postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise<any>;
postImportCiphers: (request: ImportCiphersRequest) => Promise<any>;
postImportOrganizationCiphers: (organizationId: string, request: ImportOrganizationCiphersRequest) => Promise<any>;
putDeleteCipher: (id: string) => Promise<any>;
@ -329,15 +333,15 @@ export abstract class ApiService {
getTwoFactorProviders: () => Promise<ListResponse<TwoFactorProviderResponse>>;
getTwoFactorOrganizationProviders: (organizationId: string) => Promise<ListResponse<TwoFactorProviderResponse>>;
getTwoFactorAuthenticator: (request: PasswordVerificationRequest) => Promise<TwoFactorAuthenticatorResponse>;
getTwoFactorEmail: (request: PasswordVerificationRequest) => Promise<TwoFactorEmailResponse>;
getTwoFactorDuo: (request: PasswordVerificationRequest) => Promise<TwoFactorDuoResponse>;
getTwoFactorAuthenticator: (request: SecretVerificationRequest) => Promise<TwoFactorAuthenticatorResponse>;
getTwoFactorEmail: (request: SecretVerificationRequest) => Promise<TwoFactorEmailResponse>;
getTwoFactorDuo: (request: SecretVerificationRequest) => Promise<TwoFactorDuoResponse>;
getTwoFactorOrganizationDuo: (organizationId: string,
request: PasswordVerificationRequest) => Promise<TwoFactorDuoResponse>;
getTwoFactorYubiKey: (request: PasswordVerificationRequest) => Promise<TwoFactorYubiKeyResponse>;
getTwoFactorWebAuthn: (request: PasswordVerificationRequest) => Promise<TwoFactorWebAuthnResponse>;
getTwoFactorWebAuthnChallenge: (request: PasswordVerificationRequest) => Promise<ChallengeResponse>;
getTwoFactorRecover: (request: PasswordVerificationRequest) => Promise<TwoFactorRecoverResponse>;
request: SecretVerificationRequest) => Promise<TwoFactorDuoResponse>;
getTwoFactorYubiKey: (request: SecretVerificationRequest) => Promise<TwoFactorYubiKeyResponse>;
getTwoFactorWebAuthn: (request: SecretVerificationRequest) => Promise<TwoFactorWebAuthnResponse>;
getTwoFactorWebAuthnChallenge: (request: SecretVerificationRequest) => Promise<ChallengeResponse>;
getTwoFactorRecover: (request: SecretVerificationRequest) => Promise<TwoFactorRecoverResponse>;
putTwoFactorAuthenticator: (
request: UpdateTwoFactorAuthenticatorRequest) => Promise<TwoFactorAuthenticatorResponse>;
putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise<TwoFactorEmailResponse>;
@ -384,8 +388,8 @@ export abstract class ApiService {
postLeaveOrganization: (id: string) => Promise<any>;
postOrganizationLicense: (data: FormData) => Promise<OrganizationResponse>;
postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise<any>;
postOrganizationApiKey: (id: string, request: PasswordVerificationRequest) => Promise<ApiKeyResponse>;
postOrganizationRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise<ApiKeyResponse>;
postOrganizationApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postOrganizationRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postOrganizationSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
postOrganizationUpgrade: (id: string, request: OrganizationUpgradeRequest) => Promise<PaymentResponse>;
postOrganizationUpdateSubscription: (id: string, request: OrganizationSubscriptionUpdateRequest) => Promise<void>;
@ -395,7 +399,7 @@ export abstract class ApiService {
postOrganizationVerifyBank: (id: string, request: VerifyBankRequest) => Promise<any>;
postOrganizationCancel: (id: string) => Promise<any>;
postOrganizationReinstate: (id: string) => Promise<any>;
deleteOrganization: (id: string, request: PasswordVerificationRequest) => Promise<any>;
deleteOrganization: (id: string, request: SecretVerificationRequest) => Promise<any>;
getPlans: () => Promise<ListResponse<PlanResponse>>;
getTaxRates: () => Promise<ListResponse<TaxRateResponse>>;
getOrganizationKeys: (id: string) => Promise<OrganizationKeysResponse>;
@ -449,6 +453,6 @@ export abstract class ApiService {
preValidateSso: (identifier: string) => Promise<boolean>;
getUserKeyFromCryptoAgent: (cryptoAgentUrl: string) => Promise<CryptoAgentUserKeyResponse>;
postUserKeyToCryptoAgent: (cryptoAgentUrl: string, request: CryptoAgentUserKeyRequest) => Promise<void>;
getUserKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
postUserKeyToKeyConnector: (keyConnectorUrl: string, request: KeyConnectorUserKeyRequest) => Promise<void>;
}

View File

@ -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<void>;
setUrls: (urls: any, saveSettings?: boolean) => Promise<Urls>;
getUrls: () => Urls;

View File

@ -0,0 +1,14 @@
import { Organization } from '../models/domain/organization';
export abstract class KeyConnectorService {
getAndSetKey: (url?: string) => Promise<void>;
getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>;
userNeedsMigration: () => Promise<boolean>;
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
setConvertAccountRequired: (status: boolean) => Promise<void>;
getConvertAccountRequired: () => Promise<boolean>;
removeConvertAccountRequired: () => Promise<void>;
clear: () => Promise<void>;
}

View File

@ -1,4 +1,5 @@
export abstract class PasswordRepromptService {
protectedFields: () => string[];
showPasswordPrompt: () => Promise<boolean>;
enabled: () => Promise<boolean>;
}

View File

@ -26,4 +26,5 @@ export abstract class TokenService {
getName: () => string;
getPremium: () => boolean;
getIssuer: () => string;
getIsExternal: () => boolean;
}

View File

@ -1,5 +1,6 @@
import { OrganizationData } from '../models/data/organizationData';
import { ProviderData } from '../models/data/providerData';
import { Organization } from '../models/domain/organization';
import { Provider } from '../models/domain/provider';

View File

@ -0,0 +1,9 @@
import { SecretVerificationRequest } from '../models/request/secretVerificationRequest';
import { Verification } from '../types/verification';
export abstract class UserVerificationService {
buildRequest: <T extends SecretVerificationRequest> (verification: Verification,
requestClass?: new () => T, alreadyHashed?: boolean) => Promise<T>;
verifyUser: (verification: Verification) => Promise<boolean>;
}

View File

@ -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,

View File

@ -0,0 +1,4 @@
export enum VerificationType {
MasterPassword = 0,
OTP = 1,
}

View File

@ -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');

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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;

View File

@ -0,0 +1,7 @@
export class VerifyOTPRequest {
OTP: string;
constructor(OTP: string) {
this.OTP = OTP;
}
}

View File

@ -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;
}

View File

@ -1,4 +1,4 @@
export class CryptoAgentUserKeyRequest {
export class KeyConnectorUserKeyRequest {
key: string;
constructor(key: string) {

View File

@ -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;
}

View File

@ -1,3 +0,0 @@
export class PasswordVerificationRequest {
masterPasswordHash: string;
}

View File

@ -0,0 +1,4 @@
export class SecretVerificationRequest {
masterPasswordHash: string;
otp: string;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
import { PasswordVerificationRequest } from './passwordVerificationRequest';
import { SecretVerificationRequest } from './secretVerificationRequest';
export class UpdateTwoFactorWebAuthnDeleteRequest extends PasswordVerificationRequest {
export class UpdateTwoFactorWebAuthnDeleteRequest extends SecretVerificationRequest {
id: number;
}

View File

@ -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;

View File

@ -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;

View File

@ -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');
}
}

View File

@ -1,6 +1,6 @@
import { BaseResponse } from './baseResponse';
export class CryptoAgentUserKeyResponse extends BaseResponse {
export class KeyConnectorUserKeyResponse extends BaseResponse {
key: string;
constructor(response: any) {

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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<any> {
return this.send('POST', '/accounts/set-crypto-agent-key', request, true, false);
postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise<any> {
return this.send('POST', '/accounts/set-key-connector-key', request, true, false);
}
postSecurityStamp(request: PasswordVerificationRequest): Promise<any> {
postSecurityStamp(request: SecretVerificationRequest): Promise<any> {
return this.send('POST', '/accounts/security-stamp', request, true, false);
}
deleteAccount(request: PasswordVerificationRequest): Promise<any> {
deleteAccount(request: SecretVerificationRequest): Promise<any> {
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<any> {
postAccountVerifyPassword(request: SecretVerificationRequest): Promise<any> {
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<ApiKeyResponse> {
async postUserApiKey(id: string, request: SecretVerificationRequest): Promise<ApiKeyResponse> {
const r = await this.send('POST', '/accounts/api-key', request, true, true);
return new ApiKeyResponse(r);
}
async postUserRotateApiKey(id: string, request: PasswordVerificationRequest): Promise<ApiKeyResponse> {
async postUserRotateApiKey(id: string, request: SecretVerificationRequest): Promise<ApiKeyResponse> {
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<void> {
return this.send('POST', '/accounts/request-otp', null, true, false);
}
postAccountVerifyOTP(request: VerifyOTPRequest): Promise<void> {
return this.send('POST', '/accounts/verify-otp', request, true, false);
}
postConvertToKeyConnector(): Promise<void> {
return this.send('POST', '/accounts/convert-to-key-connector', null, true, false);
}
// Folder APIs
async getFolder(id: string): Promise<FolderResponse> {
@ -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<any> {
postPurgeCiphers(request: SecretVerificationRequest, organizationId: string = null): Promise<any> {
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<TwoFactorAuthenticatorResponse> {
async getTwoFactorAuthenticator(request: SecretVerificationRequest): Promise<TwoFactorAuthenticatorResponse> {
const r = await this.send('POST', '/two-factor/get-authenticator', request, true, true);
return new TwoFactorAuthenticatorResponse(r);
}
async getTwoFactorEmail(request: PasswordVerificationRequest): Promise<TwoFactorEmailResponse> {
async getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse> {
const r = await this.send('POST', '/two-factor/get-email', request, true, true);
return new TwoFactorEmailResponse(r);
}
async getTwoFactorDuo(request: PasswordVerificationRequest): Promise<TwoFactorDuoResponse> {
async getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse> {
const r = await this.send('POST', '/two-factor/get-duo', request, true, true);
return new TwoFactorDuoResponse(r);
}
async getTwoFactorOrganizationDuo(organizationId: string,
request: PasswordVerificationRequest): Promise<TwoFactorDuoResponse> {
request: SecretVerificationRequest): Promise<TwoFactorDuoResponse> {
const r = await this.send('POST', '/organizations/' + organizationId + '/two-factor/get-duo',
request, true, true);
return new TwoFactorDuoResponse(r);
}
async getTwoFactorYubiKey(request: PasswordVerificationRequest): Promise<TwoFactorYubiKeyResponse> {
async getTwoFactorYubiKey(request: SecretVerificationRequest): Promise<TwoFactorYubiKeyResponse> {
const r = await this.send('POST', '/two-factor/get-yubikey', request, true, true);
return new TwoFactorYubiKeyResponse(r);
}
async getTwoFactorWebAuthn(request: PasswordVerificationRequest): Promise<TwoFactorWebAuthnResponse> {
async getTwoFactorWebAuthn(request: SecretVerificationRequest): Promise<TwoFactorWebAuthnResponse> {
const r = await this.send('POST', '/two-factor/get-webauthn', request, true, true);
return new TwoFactorWebAuthnResponse(r);
}
async getTwoFactorWebAuthnChallenge(request: PasswordVerificationRequest): Promise<ChallengeResponse> {
async getTwoFactorWebAuthnChallenge(request: SecretVerificationRequest): Promise<ChallengeResponse> {
const r = await this.send('POST', '/two-factor/get-webauthn-challenge', request, true, true);
return new ChallengeResponse(r);
}
async getTwoFactorRecover(request: PasswordVerificationRequest): Promise<TwoFactorRecoverResponse> {
async getTwoFactorRecover(request: SecretVerificationRequest): Promise<TwoFactorRecoverResponse> {
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<ApiKeyResponse> {
async postOrganizationApiKey(id: string, request: SecretVerificationRequest): Promise<ApiKeyResponse> {
const r = await this.send('POST', '/organizations/' + id + '/api-key', request, true, true);
return new ApiKeyResponse(r);
}
async postOrganizationRotateApiKey(id: string, request: PasswordVerificationRequest): Promise<ApiKeyResponse> {
async postOrganizationRotateApiKey(id: string, request: SecretVerificationRequest): Promise<ApiKeyResponse> {
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<any> {
deleteOrganization(id: string, request: SecretVerificationRequest): Promise<any> {
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<CryptoAgentUserKeyResponse> {
async getUserKeyFromKeyConnector(keyConnectorUrl: string): Promise<KeyConnectorUserKeyResponse> {
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<void> {
async postUserKeyToKeyConnector(keyConnectorUrl: string, request: KeyConnectorUserKeyRequest): Promise<void> {
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({

View File

@ -5,9 +5,9 @@ import { TwoFactorProviderType } from '../enums/twoFactorProviderType';
import { AuthResult } from '../models/domain/authResult';
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
import { SetCryptoAgentKeyRequest } from '../models/request/account/setCryptoAgentKeyRequest';
import { CryptoAgentUserKeyRequest } from '../models/request/cryptoAgentUserKeyRequest';
import { SetKeyConnectorKeyRequest } from '../models/request/account/setKeyConnectorKeyRequest';
import { DeviceRequest } from '../models/request/deviceRequest';
import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest';
import { KeysRequest } from '../models/request/keysRequest';
import { PreloginRequest } from '../models/request/preloginRequest';
import { TokenRequest } from '../models/request/tokenRequest';
@ -20,7 +20,9 @@ import { AppIdService } from '../abstractions/appId.service';
import { AuthService as AuthServiceAbstraction } from '../abstractions/auth.service';
import { CryptoService } from '../abstractions/crypto.service';
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { EnvironmentService } from '../abstractions/environment.service';
import { I18nService } from '../abstractions/i18n.service';
import { KeyConnectorService } from '../abstractions/keyConnector.service';
import { LogService } from '../abstractions/log.service';
import { MessagingService } from '../abstractions/messaging.service';
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
@ -101,7 +103,8 @@ export class AuthService implements AuthServiceAbstraction {
protected appIdService: AppIdService, private i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService,
private vaultTimeoutService: VaultTimeoutService, private logService: LogService,
private cryptoFunctionService: CryptoFunctionService, private setCryptoKeys = true) {
private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService,
private keyConnectorService: KeyConnectorService, private setCryptoKeys = true) {
}
init() {
@ -365,16 +368,10 @@ export class AuthService implements AuthServiceAbstraction {
// Skip this step during SSO new user flow. No key is returned from server.
if (code == null || tokenResponse.key != null) {
if (tokenResponse.cryptoAgentUrl != null) {
try {
const userKeyResponse = await this.apiService.getUserKeyFromCryptoAgent(tokenResponse.cryptoAgentUrl);
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
const k = new SymmetricCryptoKey(keyArr);
await this.cryptoService.setKey(k);
} catch (e) {
this.logService.error(e);
throw new Error('Unable to reach crypto agent');
}
if (tokenResponse.keyConnectorUrl != null) {
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
} else if (this.environmentService.getKeyConnectorUrl() != null) {
await this.keyConnectorService.getAndSetKey();
}
await this.cryptoService.setEncKey(tokenResponse.key);
@ -391,11 +388,11 @@ export class AuthService implements AuthServiceAbstraction {
}
await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey);
} else if (tokenResponse.cryptoAgentUrl != null) {
} else if (tokenResponse.keyConnectorUrl != null) {
const password = await this.cryptoFunctionService.randomBytes(64);
const k = await this.cryptoService.makeKey(Utils.fromBufferToB64(password), this.tokenService.getEmail(), tokenResponse.kdf, tokenResponse.kdfIterations);
const cryptoAgentRequest = new CryptoAgentUserKeyRequest(k.encKeyB64);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64);
await this.cryptoService.setKey(k);
const encKey = await this.cryptoService.makeEncKey(k);
@ -404,16 +401,16 @@ export class AuthService implements AuthServiceAbstraction {
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
try {
await this.apiService.postUserKeyToCryptoAgent(tokenResponse.cryptoAgentUrl, cryptoAgentRequest);
await this.apiService.postUserKeyToKeyConnector(tokenResponse.keyConnectorUrl, keyConnectorRequest);
} catch (e) {
throw new Error('Unable to reach crypto agent');
throw new Error('Unable to reach key connector');
}
const keys = new KeysRequest(pubKey, privKey.encryptedString);
const setPasswordRequest = new SetCryptoAgentKeyRequest(
const setPasswordRequest = new SetKeyConnectorKeyRequest(
encKey[1].encryptedString, tokenResponse.kdf, tokenResponse.kdfIterations, orgId, keys
);
await this.apiService.postSetCryptoAgentKey(setPasswordRequest);
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
}
}

View File

@ -19,6 +19,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
private iconsUrl: string;
private notificationsUrl: string;
private eventsUrl: string;
private keyConnectorUrl: string;
constructor(private storageService: StorageService) {}
@ -103,6 +104,10 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
return 'https://events.bitwarden.com';
}
getKeyConnectorUrl() {
return this.keyConnectorUrl;
}
async setUrlsFromStorage(): Promise<void> {
const urlsObj: any = await this.storageService.get(ConstantsService.environmentUrlsKey);
const urls = urlsObj || {
@ -113,6 +118,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
notifications: null,
events: null,
webVault: null,
keyConnector: null,
};
const envUrls = new EnvironmentUrls();
@ -128,6 +134,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
this.iconsUrl = urls.icons;
this.notificationsUrl = urls.notifications;
this.eventsUrl = envUrls.events = urls.events;
this.keyConnectorUrl = urls.keyConnector;
}
async setUrls(urls: Urls, saveSettings: boolean = true): Promise<any> {
@ -138,6 +145,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
urls.icons = this.formatUrl(urls.icons);
urls.notifications = this.formatUrl(urls.notifications);
urls.events = this.formatUrl(urls.events);
urls.keyConnector = this.formatUrl(urls.keyConnector);
if (saveSettings) {
await this.storageService.save(ConstantsService.environmentUrlsKey, {
@ -148,6 +156,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
icons: urls.icons,
notifications: urls.notifications,
events: urls.events,
keyConnector: urls.keyConnector,
});
}
@ -158,6 +167,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
this.iconsUrl = urls.icons;
this.notificationsUrl = urls.notifications;
this.eventsUrl = urls.events;
this.keyConnectorUrl = urls.keyConnector;
this.urlsSubject.next(urls);
@ -173,6 +183,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
icons: this.iconsUrl,
notifications: this.notificationsUrl,
events: this.eventsUrl,
keyConnector: this.keyConnectorUrl,
};
}

View File

@ -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<boolean> {
return this.usesKeyConnector ??= await this.storageService.get<boolean>(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<boolean> {
return await this.storageService.get(Keys.convertAccountToKeyConnector);
}
async removeConvertAccountRequired() {
await this.storageService.remove(Keys.convertAccountToKeyConnector);
}
async clear() {
await this.removeConvertAccountRequired();
}
}

View File

@ -3,6 +3,7 @@ import { CipherService } from '../abstractions/cipher.service';
import { CollectionService } from '../abstractions/collection.service';
import { CryptoService } from '../abstractions/crypto.service';
import { FolderService } from '../abstractions/folder.service';
import { KeyConnectorService } from '../abstractions/keyConnector.service';
import { LogService } from '../abstractions/log.service';
import { MessagingService } from '../abstractions/messaging.service';
import { PolicyService } from '../abstractions/policy.service';
@ -10,6 +11,7 @@ import { SendService } from '../abstractions/send.service';
import { SettingsService } from '../abstractions/settings.service';
import { StorageService } from '../abstractions/storage.service';
import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service';
import { TokenService } from '../abstractions/token.service';
import { UserService } from '../abstractions/user.service';
import { CipherData } from '../models/data/cipherData';
@ -46,6 +48,7 @@ export class SyncService implements SyncServiceAbstraction {
private collectionService: CollectionService, private storageService: StorageService,
private messagingService: MessagingService, private policyService: PolicyService,
private sendService: SendService, private logService: LogService,
private tokenService: TokenService, private keyConnectorService: KeyConnectorService,
private logoutCallback: (expired: boolean) => Promise<void>) {
}
@ -299,6 +302,7 @@ export class SyncService implements SyncServiceAbstraction {
await this.userService.setSecurityStamp(response.securityStamp);
await this.userService.setEmailVerified(response.emailVerified);
await this.userService.setForcePasswordReset(response.forcePasswordReset);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
const organizations: { [id: string]: OrganizationData; } = {};
response.organizations.forEach(o => {
@ -316,6 +320,13 @@ export class SyncService implements SyncServiceAbstraction {
organizations[o.id].isProviderUser = true;
}
});
if (await this.keyConnectorService.userNeedsMigration()) {
this.messagingService.send('convertAccountToKeyConnector');
} else {
this.keyConnectorService.removeConvertAccountRequired();
}
return Promise.all([
this.userService.replaceOrganizations(organizations),
this.userService.replaceProviders(providers),

View File

@ -243,6 +243,15 @@ export class TokenService implements TokenServiceAbstraction {
return decoded.iss as string;
}
getIsExternal(): boolean {
const decoded = this.decodeToken();
if (!Array.isArray(decoded.amr)) {
throw new Error('No amr found');
}
return decoded.amr.includes('external');
}
private async storeTokenValue(key: string, value: string) {
if (await this.skipTokenStorage()) {
// if we have a vault timeout and the action is log out, don't store token

View File

@ -6,6 +6,7 @@ import { OrganizationData } from '../models/data/organizationData';
import { Organization } from '../models/domain/organization';
import { KdfType } from '../enums/kdfType';
import { ProviderData } from '../models/data/providerData';
import { Provider } from '../models/domain/provider';

View File

@ -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<T extends SecretVerificationRequest>(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<boolean> {
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;
}
}

View File

@ -4,6 +4,7 @@ import { CipherService } from '../abstractions/cipher.service';
import { CollectionService } from '../abstractions/collection.service';
import { CryptoService } from '../abstractions/crypto.service';
import { FolderService } from '../abstractions/folder.service';
import { KeyConnectorService } from '../abstractions/keyConnector.service';
import { MessagingService } from '../abstractions/messaging.service';
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
import { PolicyService } from '../abstractions/policy.service';
@ -28,6 +29,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
protected platformUtilsService: PlatformUtilsService, private storageService: StorageService,
private messagingService: MessagingService, private searchService: SearchService,
private userService: UserService, private tokenService: TokenService, private policyService: PolicyService,
private keyConnectorService: KeyConnectorService,
private lockedCallback: () => Promise<void> = null, private loggedOutCallback: () => Promise<void> = null) {
}
@ -98,6 +100,15 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
return;
}
if (await this.keyConnectorService.getUsesKeyConnector()) {
const pinSet = await this.isPinLockSet();
const pinLock = (pinSet[0] && this.pinProtectedKey != null) || pinSet[1];
if (!pinLock && !await this.isBiometricLockSet()) {
await this.logOut();
}
}
this.biometricLocked = true;
this.everBeenUnlocked = true;
await this.cryptoService.clearKey(false);

View File

@ -0,0 +1,6 @@
import { VerificationType } from '../enums/verificationType';
export type Verification = {
type: VerificationType,
secret: string,
};

View File

@ -14,6 +14,7 @@ import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
@ -22,6 +23,7 @@ import { UserService } from 'jslib-common/abstractions/user.service';
import { Response } from '../models/response';
import { KeyConnectorUserKeyRequest } from 'jslib-common/models/request/keyConnectorUserKeyRequest';
import { UpdateTempPasswordRequest } from 'jslib-common/models/request/updateTempPasswordRequest';
import { MessageResponse } from '../models/response/messageResponse';
@ -48,7 +50,8 @@ export class LoginCommand {
protected passwordGenerationService: PasswordGenerationService,
protected cryptoFunctionService: CryptoFunctionService, protected platformUtilsService: PlatformUtilsService,
protected userService: UserService, protected cryptoService: CryptoService,
protected policyService: PolicyService, clientId: string, private syncService: SyncService) {
protected policyService: PolicyService, clientId: string, private syncService: SyncService,
protected keyConnectorService: KeyConnectorService) {
this.clientId = clientId;
}
@ -57,6 +60,7 @@ export class LoginCommand {
let ssoCodeVerifier: string = null;
let ssoCode: string = null;
let orgIdentifier: string = null;
let clientId: string = null;
let clientSecret: string = null;
@ -79,7 +83,9 @@ export class LoginCommand {
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256');
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
try {
ssoCode = await this.getSsoCode(codeChallenge, state);
const ssoParams = await this.openSsoPrompt(codeChallenge, state);
ssoCode = ssoParams.ssoCode;
orgIdentifier = ssoParams.orgIdentifier;
} catch {
return Response.badRequest('Something went wrong. Try again.');
}
@ -151,7 +157,8 @@ export class LoginCommand {
if (clientId != null && clientSecret != null) {
response = await this.authService.logInApiKey(clientId, clientSecret);
} else if (ssoCode != null && ssoCodeVerifier != null) {
response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri, null);
response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri,
orgIdentifier);
} else {
response = await this.authService.logIn(email, password);
}
@ -220,8 +227,9 @@ export class LoginCommand {
if (twoFactorToken == null && response.twoFactorProviders.size > 1 &&
selectedProvider.type === TwoFactorProviderType.Email) {
const emailReq = new TwoFactorEmailRequest(this.authService.email,
this.authService.masterPasswordHash);
const emailReq = new TwoFactorEmailRequest();
emailReq.email = this.authService.email;
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
await this.apiService.postTwoFactorEmail(emailReq);
}
@ -254,9 +262,16 @@ export class LoginCommand {
' through the web vault to set your master password.');
}
// Full sync required for the reset password and key connector checks
await this.syncService.fullSync(true);
// Handle converting to Key Connector if required
if (await this.keyConnectorService.userNeedsMigration()) {
return await this.migrateToKeyConnector();
}
// Handle Updating Temp Password if NOT using an API Key for authentication
if (response.forcePasswordReset && (clientId == null && clientSecret == null)) {
await this.syncService.fullSync(true);
return await this.updateTempPassword();
}
@ -383,6 +398,57 @@ export class LoginCommand {
return userInput;
}
private async migrateToKeyConnector() {
// If no interaction available, alert user to use web vault
if (!this.canInteract) {
await this.logout();
this.authService.logOut(() => { /* Do nothing */ });
return Response.error(new MessageResponse('An organization you are a member of is using Key Connector. ' +
'In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.', null));
}
const organization = await this.keyConnectorService.getManagingOrganization();
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: 'list',
name: 'convert',
message: organization.name + ' is using a self-hosted key server. A master password is no longer required to log in for members of this organization. ',
choices: [
{
name: 'Remove master password and log in',
value: 'remove',
},
{
name: 'Leave organization and log in',
value: 'leave',
},
{
name: 'Exit',
value: 'exit',
},
],
});
if (answer.convert === 'remove') {
await this.keyConnectorService.migrateUser();
// Update environment URL - required for api key login
const urls = this.environmentService.getUrls();
urls.keyConnector = organization.keyConnectorUrl;
await this.environmentService.setUrls(urls, true);
return await this.handleSuccessResponse();
} else if (answer.convert === 'leave') {
await this.apiService.postLeaveOrganization(organization.id);
await this.syncService.fullSync(true);
return await this.handleSuccessResponse();
} else {
await this.logout();
this.authService.logOut(() => { /* Do nothing */ });
return Response.error('You have been logged out.');
}
}
private async apiClientId(): Promise<string> {
let clientId: string = null;
@ -432,13 +498,14 @@ export class LoginCommand {
};
}
private async getSsoCode(codeChallenge: string, state: string): Promise<string> {
private async openSsoPrompt(codeChallenge: string, state: string): Promise<{ ssoCode: string, orgIdentifier: string }> {
return new Promise((resolve, reject) => {
const callbackServer = http.createServer((req, res) => {
const urlString = 'http://localhost' + req.url;
const url = new URL(urlString);
const code = url.searchParams.get('code');
const receivedState = url.searchParams.get('state');
const orgIdentifier = this.getOrgIdentifierFromState(receivedState);
res.setHeader('Content-Type', 'text/html');
if (code != null && receivedState != null && this.checkState(receivedState, state)) {
res.writeHead(200);
@ -446,7 +513,10 @@ export class LoginCommand {
'<h1>Successfully authenticated with the Bitwarden CLI</h1>' +
'<p>You may now close this tab and return to the terminal.</p>' +
'</body></html>');
callbackServer.close(() => resolve(code));
callbackServer.close(() => resolve({
ssoCode: code,
orgIdentifier: orgIdentifier,
}));
} else {
res.writeHead(400);
res.end('<html><head><title>Failed | Bitwarden CLI</title></head><body>' +
@ -478,6 +548,15 @@ export class LoginCommand {
});
}
private getOrgIdentifierFromState(state: string): string {
if (state === null || state === undefined) {
return null;
}
const stateSplit = state.split('_identifier=');
return stateSplit.length > 1 ? stateSplit[1] : null;
}
private checkState(state: string, checkState: string): boolean {
if (state === null || state === undefined) {
return false;