From 09ef907673976002f9d04fd5fbfee3e74ed226e2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Jun 2018 09:20:09 -0400 Subject: [PATCH] authenticator setup --- jslib | 2 +- src/app/app.module.ts | 3 + .../two-factor-authenticator.component.html | 83 +++++++++++++ .../two-factor-authenticator.component.ts | 116 ++++++++++++++++++ .../settings/two-factor-setup.component.html | 9 +- .../settings/two-factor-setup.component.ts | 64 ++++++++-- src/locales/en/messages.json | 48 ++++++++ 7 files changed, 316 insertions(+), 9 deletions(-) create mode 100644 src/app/settings/two-factor-authenticator.component.html create mode 100644 src/app/settings/two-factor-authenticator.component.ts diff --git a/jslib b/jslib index 3cc759791e..ec505b8c55 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 3cc759791e12b7692fc2d2b4be1a2b010eee1c8e +Subproject commit ec505b8c5508e4c8f6191c946b042a804c73c501 diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9a996ab872..5b557fcc72 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -42,6 +42,7 @@ import { OptionsComponent } from './settings/options.component'; import { ProfileComponent } from './settings/profile.component'; import { PurgeVaultComponent } from './settings/purge-vault.component'; import { SettingsComponent } from './settings/settings.component'; +import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component'; import { TwoFactorSetupComponent } from './settings/two-factor-setup.component'; import { ExportComponent } from './tools/export.component'; @@ -143,6 +144,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe'; StopPropDirective, ToolsComponent, TrueFalseValueDirective, + TwoFactorAuthenticatorComponent, TwoFactorComponent, TwoFactorOptionsComponent, TwoFactorSetupComponent, @@ -163,6 +165,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe'; PasswordGeneratorHistoryComponent, PurgeVaultComponent, ShareComponent, + TwoFactorAuthenticatorComponent, TwoFactorOptionsComponent, ], providers: [], diff --git a/src/app/settings/two-factor-authenticator.component.html b/src/app/settings/two-factor-authenticator.component.html new file mode 100644 index 0000000000..cf6ef3b21a --- /dev/null +++ b/src/app/settings/two-factor-authenticator.component.html @@ -0,0 +1,83 @@ + diff --git a/src/app/settings/two-factor-authenticator.component.ts b/src/app/settings/two-factor-authenticator.component.ts new file mode 100644 index 0000000000..ed67c90883 --- /dev/null +++ b/src/app/settings/two-factor-authenticator.component.ts @@ -0,0 +1,116 @@ +import { + Component, + EventEmitter, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { ApiService } from 'jslib/abstractions/api.service'; +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest'; +import { TwoFactorProviderRequest } from 'jslib/models/request/twoFactorProviderRequest'; +import { UpdateTwoFactorAuthenticatorRequest } from 'jslib/models/request/updateTwoFactorAuthenticatorRequest'; +import { TwoFactorAuthenticatorResponse } from 'jslib/models/response/twoFactorAuthenticatorResponse'; + +import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType'; + +@Component({ + selector: 'app-two-factor-authenticator', + templateUrl: 'two-factor-authenticator.component.html', +}) +export class TwoFactorAuthenticatorComponent { + @Output() onUpdated = new EventEmitter(); + + enabled = false; + authed = false; + key: string; + qr: string; + token: string; + masterPassword: string; + + authPromise: Promise; + formPromise: Promise; + + private masterPasswordHash: string; + + constructor(private apiService: ApiService, private i18nService: I18nService, + private analytics: Angulartics2, private toasterService: ToasterService, + private cryptoService: CryptoService, private userService: UserService, + private platformUtilsService: PlatformUtilsService) { } + + async auth() { + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + + const request = new PasswordVerificationRequest(); + request.masterPasswordHash = this.masterPasswordHash = + await this.cryptoService.hashPassword(this.masterPassword, null); + try { + this.authPromise = this.apiService.getTwoFactorAuthenticator(request); + const response = await this.authPromise; + this.authed = true; + await this.processResponse(response); + } catch { } + } + + async submit() { + if (this.enabled) { + this.disable(); + } else { + this.enable(); + } + } + + private async enable() { + const request = new UpdateTwoFactorAuthenticatorRequest(); + request.masterPasswordHash = this.masterPasswordHash; + request.token = this.token; + request.key = this.key; + try { + this.formPromise = this.apiService.putTwoFactorAuthenticator(request); + const response = await this.formPromise; + await this.processResponse(response); + this.analytics.eventTrack.next({ action: 'Enabled Two-step Authenticator' }); + this.onUpdated.emit(true); + } catch { } + } + + private async disable() { + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('twoStepDisableDesc'), + this.i18nService.t('disable'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return; + } + + try { + const request = new TwoFactorProviderRequest(); + request.masterPasswordHash = this.masterPasswordHash; + request.type = TwoFactorProviderType.Authenticator; + this.formPromise = this.apiService.putTwoFactorDisable(request); + await this.formPromise; + this.enabled = false; + this.analytics.eventTrack.next({ action: 'Disabled Two-step Authenticator' }); + this.toasterService.popAsync('success', null, this.i18nService.t('twoStepDisabled')); + this.onUpdated.emit(false); + } catch { } + } + + private async processResponse(response: TwoFactorAuthenticatorResponse) { + this.token = null; + this.enabled = response.enabled; + this.key = response.key; + this.qr = 'https://chart.googleapis.com/chart?chs=160x160&chld=L|0&cht=qr&chl=otpauth://totp/' + + 'Bitwarden:' + encodeURIComponent(await this.userService.getEmail()) + + '%3Fsecret=' + encodeURIComponent(this.key) + + '%26issuer=Bitwarden'; + } +} diff --git a/src/app/settings/two-factor-setup.component.html b/src/app/settings/two-factor-setup.component.html index c7faabe757..13a9e077d0 100644 --- a/src/app/settings/two-factor-setup.component.html +++ b/src/app/settings/two-factor-setup.component.html @@ -27,9 +27,16 @@ {{p.description}}
-
+ + + + + + + diff --git a/src/app/settings/two-factor-setup.component.ts b/src/app/settings/two-factor-setup.component.ts index 0274577f20..cba828e1c5 100644 --- a/src/app/settings/two-factor-setup.component.ts +++ b/src/app/settings/two-factor-setup.component.ts @@ -1,31 +1,42 @@ import { Component, + ComponentFactoryResolver, OnInit, + Type, + ViewChild, + ViewContainerRef, } from '@angular/core'; -import { ToasterService } from 'angular2-toaster'; -import { Angulartics2 } from 'angulartics2'; - import { ApiService } from 'jslib/abstractions/api.service'; -import { I18nService } from 'jslib/abstractions/i18n.service'; import { TokenService } from 'jslib/abstractions/token.service'; import { TwoFactorProviders } from 'jslib/services/auth.service'; import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType'; +import { ModalComponent } from '../modal.component'; + +import { TwoFactorAuthenticatorComponent } from './two-factor-authenticator.component'; + @Component({ selector: 'app-two-factor-setup', templateUrl: 'two-factor-setup.component.html', }) export class TwoFactorSetupComponent implements OnInit { + @ViewChild('recoveryTemplate', { read: ViewContainerRef }) recoveryModalRef: ViewContainerRef; + @ViewChild('authenticatorTemplate', { read: ViewContainerRef }) authenticatorModalRef: ViewContainerRef; + @ViewChild('yubikeyTemplate', { read: ViewContainerRef }) yubikeyModalRef: ViewContainerRef; + @ViewChild('u2fTemplate', { read: ViewContainerRef }) u2fModalRef: ViewContainerRef; + @ViewChild('duoTemplate', { read: ViewContainerRef }) duoModalRef: ViewContainerRef; + providers: any[] = []; premium: boolean; loading = true; - constructor(private apiService: ApiService, private i18nService: I18nService, - private analytics: Angulartics2, private toasterService: ToasterService, - private tokenService: TokenService) { } + private modal: ModalComponent = null; + + constructor(private apiService: ApiService, private tokenService: TokenService, + private componentFactoryResolver: ComponentFactoryResolver) { } async ngOnInit() { this.premium = this.tokenService.getPremium(); @@ -66,4 +77,43 @@ export class TwoFactorSetupComponent implements OnInit { }); this.loading = false; } + + manage(type: TwoFactorProviderType) { + switch (type) { + case TwoFactorProviderType.Authenticator: + const component = this.openModal(this.authenticatorModalRef, TwoFactorAuthenticatorComponent); + component.onUpdated.subscribe((enabled: boolean) => { + this.updateStatus(enabled, TwoFactorProviderType.Authenticator) + }); + break; + default: + break; + } + } + + private openModal(ref: ViewContainerRef, type: Type): T { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = ref.createComponent(factory).instance; + const childComponent = this.modal.show(type, ref); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + return childComponent; + } + + private updateStatus(enabled: boolean, type: TwoFactorProviderType) { + if (!enabled && this.modal != null) { + this.modal.close(); + } + this.providers.forEach((p) => { + if (p.type === type) { + p.enabled = enabled; + } + }); + } } diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 0ed3a8262b..9747d6d5de 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -978,5 +978,53 @@ }, "manage": { "message": "Manage" + }, + "disable": { + "message": "Disable" + }, + "twoStepLoginProviderEnabled": { + "message": "This two-step login provider is enabled on your account." + }, + "twoStepLoginAuthDesc": { + "message": "Enter your master password to modify two-step login settings." + }, + "twoStepAuthenticatorDesc": { + "message": "Follow these steps to set up two-step login with an authenticator app:" + }, + "twoStepAuthenticatorDownloadApp": { + "message": "Download a two-step authenticator app" + }, + "twoStepAuthenticatorNeedApp": { + "message": "Need a two-step authenticator app? Download one of the following" + }, + "iosDevices": { + "message": "iOS devices" + }, + "androidDevices": { + "message": "Android devices" + }, + "windowsDevices": { + "message": "Windows devices" + }, + "twoStepAuthenticatorAppsRecommended": { + "message": "These apps are recommended, however, other authenticator apps will also work." + }, + "twoStepAuthenticatorScanCode": { + "message": "Scan this QR code with your authenticator app" + }, + "key": { + "message": "Key" + }, + "twoStepAuthenticatorEnterCode": { + "message": "Enter the resulting 6 digit verification code from the app" + }, + "twoStepAuthenticatorReaddDesc": { + "message": "In case you need to add it to another device, below is the QR code (or key) required by your authenticator app." + }, + "twoStepDisableDesc": { + "message": "Are you sure you want to disable this two-step login provider?" + }, + "twoStepDisabled": { + "message": "Two-step login provider disabled." } }