diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6dc17f13ef..1d811196f0 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -55,6 +55,7 @@ import { PasswordGeneratorComponent } from './tools/password-generator.component import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component'; import { ToolsComponent } from './tools/tools.component'; import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component'; +import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component'; import { VaultComponent } from './vault/vault.component'; @@ -160,6 +161,11 @@ const routes: Routes = [ component: UnsecuredWebsitesReportComponent, data: { titleId: 'unsecuredWebsitesReport' }, }, + { + path: 'weak-passwords-report', + component: WeakPasswordsReportComponent, + data: { titleId: 'weakPasswordsReport' }, + }, ], }, ], diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6bb182743b..edd41512c0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -111,6 +111,7 @@ import { PasswordGeneratorComponent } from './tools/password-generator.component import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component'; import { ToolsComponent } from './tools/tools.component'; import { UnsecuredWebsitesReportComponent } from './tools/unsecured-websites-report.component'; +import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.component'; import { AddEditComponent } from './vault/add-edit.component'; import { AttachmentsComponent } from './vault/attachments.component'; @@ -301,6 +302,7 @@ registerLocaleData(localeZhCn, 'zh-CN'); VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + WeakPasswordsReportComponent, ], entryComponents: [ AddEditComponent, diff --git a/src/app/tools/tools.component.html b/src/app/tools/tools.component.html index ec9a6b1a4f..6459be9b08 100644 --- a/src/app/tools/tools.component.html +++ b/src/app/tools/tools.component.html @@ -27,6 +27,9 @@ {{'unsecuredWebsitesReport' | i18n}} + + {{'weakPasswordsReport' | i18n}} + diff --git a/src/app/tools/weak-passwords-report.component.html b/src/app/tools/weak-passwords-report.component.html new file mode 100644 index 0000000000..df62ad52b9 --- /dev/null +++ b/src/app/tools/weak-passwords-report.component.html @@ -0,0 +1,44 @@ + +

{{'weakPasswordsReportDesc' | i18n}}

+
+ +
+
+ + {{'noWeakPasswords'}} + + + + {{'weakPasswordsFoundDesc' | i18n : ciphers.length}} + + + + + + + + + +
+ + + {{c.name}} + + +
+ {{c.subTitle}} +
+ + {{passwordStrengthMap.get(c.id)[0] | i18n}} + +
+
+
+ diff --git a/src/app/tools/weak-passwords-report.component.ts b/src/app/tools/weak-passwords-report.component.ts new file mode 100644 index 0000000000..93562307d3 --- /dev/null +++ b/src/app/tools/weak-passwords-report.component.ts @@ -0,0 +1,98 @@ +import { + Component, + ComponentFactoryResolver, + OnInit, + ViewChild, + ViewContainerRef, +} from '@angular/core'; + +import { CipherService } from 'jslib/abstractions/cipher.service'; +import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.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'; + +@Component({ + selector: 'app-weak-passwords-report', + templateUrl: 'weak-passwords-report.component.html', +}) +export class WeakPasswordsReportComponent implements OnInit { + @ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef; + + loading = true; + hasLoaded = false; + ciphers: CipherView[] = []; + passwordStrengthMap = new Map(); + + private modal: ModalComponent = null; + + constructor(private ciphersService: CipherService, private passwordGenerationService: PasswordGenerationService, + private componentFactoryResolver: ComponentFactoryResolver) { } + + async ngOnInit() { + await this.load(); + this.hasLoaded = true; + } + + async load() { + this.loading = true; + const allCiphers = await this.ciphersService.getAllDecrypted(); + const weakPasswordCiphers: CipherView[] = []; + allCiphers.forEach((c) => { + if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') { + return; + } + const result = this.passwordGenerationService.passwordStrength(c.login.password); + if (result.score <= 3) { + this.passwordStrengthMap.set(c.id, this.scoreKey(result.score)); + weakPasswordCiphers.push(c); + } + }); + this.ciphers = weakPasswordCiphers; + this.loading = false; + } + + 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 scoreKey(score: number): [string, string] { + switch (score) { + case 4: + return ['strong', 'success']; + case 3: + return ['good', 'primary']; + case 2: + return ['weak', 'warning']; + default: + return ['veryWeak', 'danger']; + } + } +} diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 18afae9c44..ab8befa1d0 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -1308,6 +1308,27 @@ "noUnsecuredWebsites": { "message": "No items in your vault have unsecured URIs." }, + "weakPasswordsReport": { + "message": "Weak Passwords Report" + }, + "weakPasswordsReportDesc": { + "message": "Weak passwords can easily be guessed by hackers and automated tools that brute force passwords." + }, + "weakPasswordsFound": { + "message": "Weak Passwords Found" + }, + "weakPasswordsFoundDesc": { + "message": "We found $COUNT$ items in your vault that have weak passwords. You should update them to use stronger passwords.", + "placeholders": { + "count": { + "content": "$1", + "example": "8" + } + } + }, + "noWeakPasswords": { + "message": "No items in your vault have weak passwords." + }, "reusedPasswordsReport": { "message": "Reused Passwords Report" }, @@ -2585,15 +2606,19 @@ }, "strong": { "message": "Strong", - "description": "ex. A strong password. Scale: Weak -> Good -> Strong" + "description": "ex. A strong password. Scale: Very Weak -> Weak -> Good -> Strong" }, "good": { "message": "Good", - "description": "ex. A good password. Scale: Weak -> Good -> Strong" + "description": "ex. A good password. Scale: Very Weak -> Weak -> Good -> Strong" }, "weak": { "message": "Weak", - "description": "ex. A weak password. Scale: Weak -> Good -> Strong" + "description": "ex. A weak password. Scale: Very Weak -> Weak -> Good -> Strong" + }, + "veryWeak": { + "message": "Very Weak", + "description": "ex. A very weak password. Scale: Very Weak -> Weak -> Good -> Strong" }, "weakMasterPassword": { "message": "Weak Master Password"