diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f45f4f33c8..c624f52bfe 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -52,6 +52,7 @@ import { BreachReportComponent } from './tools/breach-report.component'; import { ExportComponent } from './tools/export.component'; import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component'; import { ImportComponent } from './tools/import.component'; +import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component'; import { PasswordGeneratorComponent } from './tools/password-generator.component'; import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component'; import { ToolsComponent } from './tools/tools.component'; @@ -172,6 +173,11 @@ const routes: Routes = [ component: ExposedPasswordsReportComponent, data: { titleId: 'exposedPasswordsReport' }, }, + { + path: 'inactive-two-factor-report', + component: InactiveTwoFactorReportComponent, + data: { titleId: 'exposedPasswordsReport' }, + }, ], }, ], diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2a232c323b..0042298cd4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -107,6 +107,7 @@ import { BreachReportComponent } from './tools/breach-report.component'; import { ExportComponent } from './tools/export.component'; import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component'; import { ImportComponent } from './tools/import.component'; +import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component'; import { PasswordGeneratorHistoryComponent } from './tools/password-generator-history.component'; import { PasswordGeneratorComponent } from './tools/password-generator.component'; import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component'; @@ -230,9 +231,10 @@ registerLocaleData(localeZhCn, 'zh-CN'); FrontendLayoutComponent, GroupingsComponent, HintComponent, - IconComponent, I18nPipe, + IconComponent, ImportComponent, + InactiveTwoFactorReportComponent, InputVerbatimDirective, LockComponent, LoginComponent, diff --git a/src/app/tools/inactive-two-factor-report.component.html b/src/app/tools/inactive-two-factor-report.component.html new file mode 100644 index 0000000000..acf4931497 --- /dev/null +++ b/src/app/tools/inactive-two-factor-report.component.html @@ -0,0 +1,44 @@ + +

{{'inactive2faReportDesc' | i18n}}

+
+ +
+
+ + {{'noInactive2fa'}} + + + + {{'inactive2faFoundDesc' | i18n : (ciphers.length | number)}} + + + + + + + + + +
+ + + {{c.name}} + + +
+ {{c.subTitle}} +
+ + {{'2faInstructions' | i18n}} +
+
+
+ diff --git a/src/app/tools/inactive-two-factor-report.component.ts b/src/app/tools/inactive-two-factor-report.component.ts new file mode 100644 index 0000000000..f1dbe67256 --- /dev/null +++ b/src/app/tools/inactive-two-factor-report.component.ts @@ -0,0 +1,130 @@ +import { + Component, + ComponentFactoryResolver, + OnInit, + ViewChild, + ViewContainerRef, +} from '@angular/core'; + +import { CipherService } from 'jslib/abstractions/cipher.service'; + +import { CipherView } from 'jslib/models/view/cipherView'; + +import { CipherType } from 'jslib/enums/cipherType'; + +import { ModalComponent } from '../modal.component'; +import { AddEditComponent } from '../vault/add-edit.component'; + +import { Utils } from 'jslib/misc/utils'; + +@Component({ + selector: 'app-inactive-two-factor-report', + templateUrl: 'inactive-two-factor-report.component.html', +}) +export class InactiveTwoFactorReportComponent implements OnInit { + @ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef; + + loading = false; + hasLoaded = false; + services = new Map(); + cipherDocs = new Map(); + ciphers: CipherView[] = []; + + private modal: ModalComponent = null; + + constructor(private ciphersService: CipherService, private componentFactoryResolver: ComponentFactoryResolver) { } + + async ngOnInit() { + await this.load(); + } + + async load() { + this.loading = true; + try { + await this.load2fa(); + } catch { } + if (this.services.size > 0) { + const allCiphers = await this.ciphersService.getAllDecrypted(); + const inactive2faCiphers: CipherView[] = []; + const promises: Array> = []; + const docs = new Map(); + allCiphers.forEach((c) => { + if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris) { + return; + } + for (let i = 0; i < c.login.uris.length; i++) { + const u = c.login.uris[i]; + if (u.uri != null && u.uri !== '') { + const hostname = Utils.getHostname(u.uri); + if (hostname != null && this.services.has(hostname)) { + if (this.services.get(hostname) != null) { + docs.set(c.id, this.services.get(hostname)); + } + inactive2faCiphers.push(c); + break; + } + } + } + }); + await Promise.all(promises); + this.ciphers = inactive2faCiphers; + this.cipherDocs = docs; + } + this.loading = false; + this.hasLoaded = true; + } + + selectCipher(cipher: CipherView) { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.cipherAddEditModalRef.createComponent(factory).instance; + const childComponent = this.modal.show( + AddEditComponent, this.cipherAddEditModalRef); + + childComponent.cipherId = cipher == null ? null : cipher.id; + childComponent.onSavedCipher.subscribe(async (c: CipherView) => { + this.modal.close(); + await this.load(); + }); + childComponent.onDeletedCipher.subscribe(async (c: CipherView) => { + this.modal.close(); + await this.load(); + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + + return childComponent; + } + + private async load2fa() { + if (this.services.size > 0) { + return; + } + const response = await fetch(new Request('https://twofactorauth.org/data.json')); + if (response.status !== 200) { + throw new Error(); + } + const responseJson = await response.json(); + for (const categoryName in responseJson) { + if (responseJson.hasOwnProperty(categoryName)) { + const category = responseJson[categoryName]; + for (const serviceName in category) { + if (category.hasOwnProperty(serviceName)) { + const service = category[serviceName]; + if (service.tfa && service.url != null) { + const hostname = Utils.getHostname(service.url); + if (hostname != null) { + this.services.set(hostname, service.doc); + } + } + } + } + } + } + } +} diff --git a/src/app/tools/tools.component.html b/src/app/tools/tools.component.html index d7fc696014..18fbc988a8 100644 --- a/src/app/tools/tools.component.html +++ b/src/app/tools/tools.component.html @@ -33,6 +33,9 @@ {{'exposedPasswordsReport' | i18n}} + + {{'inactive2faReport' | i18n}} + diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 29b697f6f6..0b39f9f3e1 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -1308,6 +1308,30 @@ "noUnsecuredWebsites": { "message": "No items in your vault have unsecured URIs." }, + "inactive2faReport": { + "message": "Inactive 2FA Report" + }, + "inactive2faReportDesc": { + "message": "Two-factor authentication (2FA) is an important security setting that helps secure your accounts. If the website offers it, you should always enable two-factor authentication." + }, + "inactive2faFound": { + "message": "Logins Without 2FA Found" + }, + "inactive2faFoundDesc": { + "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-factor authentication (according to twofactorauth.org). To further protect your accounts, you should always use two-factor authentication.", + "placeholders": { + "count": { + "content": "$1", + "example": "8" + } + } + }, + "noInactive2fa": { + "message": "No websites were found in your vault with a missing two-factor authentication configuration." + }, + "2faInstructions": { + "message": "2FA Instructions" + }, "exposedPasswordsReport": { "message": "Exposed Passwords Report" },