password quality warnings

vibrancy
antelle 3 years ago
parent 1796ac743d
commit 01fac1f0ca
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C

@ -0,0 +1,37 @@
import kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
const logger = new Logger('online-password-checker');
const exposedPasswords = {};
function checkIfPasswordIsExposedOnline(password) {
if (!password || !password.isProtected || !password.byteLength) {
return false;
}
const saltedValue = password.saltedValue();
const cached = exposedPasswords[saltedValue];
if (cached !== undefined) {
return cached;
}
const passwordBytes = password.getBinary();
return crypto.subtle
.digest({ name: 'SHA-1' }, passwordBytes)
.then((sha1) => {
kdbxweb.ByteUtils.zeroBuffer(passwordBytes);
sha1 = kdbxweb.ByteUtils.bytesToHex(sha1).toUpperCase();
const shaFirst = sha1.substr(0, 5);
return fetch(`https://api.pwnedpasswords.com/range/${shaFirst}`)
.then((response) => response.text())
.then((response) => {
const isPresent = response.includes(sha1.substr(5));
exposedPasswords[saltedValue] = isPresent;
return isPresent;
});
})
.catch((e) => {
logger.error('Error checking password online', e);
});
}
export { checkIfPasswordIsExposedOnline };

@ -37,6 +37,10 @@ const DefaultAppSettings = {
useGroupIconForEntries: false, // automatically use group icon when creating new entries
enableUsb: true, // enable interaction with USB devices
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
auditPasswords: true, // enable password audit
excludePinsFromAudit: true, // exclude PIN codes from audit
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
auditPasswordAge: 0,
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files

@ -17,7 +17,9 @@ const Links = {
Plugins: 'https://plugins.keeweb.info',
PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins',
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation'
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
HaveIBeenPwned: 'https://haveibeenpwned.com',
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords'
};
export { Links };

@ -16,7 +16,8 @@ const Timeouts = {
ExternalDeviceReconnect: 3000,
ExternalDeviceAfterReconnect: 1000,
FieldLabelDoubleClick: 300,
NativeModuleHostRestartTime: 3000
NativeModuleHostRestartTime: 3000,
FastAnimation: 100
};
export { Timeouts };

@ -301,6 +301,12 @@
"detRevealField": "Reveal",
"detHideField": "Hide",
"detAutoTypeField": "Auto type",
"detIssuesHideTooltip": "Hide this warning",
"detIssueWeakPassword": "The password is weak, it's recommended to change it.",
"detIssuePoorPassword": "The password is very weak, it's strongly recommended to change it.",
"detIssuePwnedPassword": "This password has been exposed in a data breach according to {}, it's recommended to change it.",
"detIssuePasswordCheckError": "There was an error checking password strength online.",
"detIssueOldPassword": "The password is old.",
"autoTypeEntryFields": "Entry fields",
"autoTypeModifiers": "Modifier keys",
@ -442,6 +448,16 @@
"setGenShowAppLogs": "Show app logs",
"setGenReloadApp": "Reload the app",
"setGenFieldLabelDblClickAutoType": "Auto-type on double-clicking field labels",
"setGenAudit": "Audit",
"setGenAuditPasswords": "Show warnings about password strength",
"setGenExcludePinsFromAudit": "Never check short numeric PIN codes, such as 123456",
"setGenCheckPasswordsOnHIBP": "Check passwords using an online service {}",
"setGenHelpHIBP": "KeeWeb can check if your passwords have been previously exposed in a data breach using an online service. Your password cannot be recovered based on data sent online, however the number of passwords checked this way may be exposed. More about your privacy when using this service can be found {}. If this option is enabled, KeeWeb will automatically check your passwords there.",
"setGenHelpHIBPLink": "here",
"setGenAuditPasswordAge": "Old passwords",
"setGenAuditPasswordAgeOff": "Don't show warnings about old passwords",
"setGenAuditPasswordAgeOneYear": "Show warnings for passwords older than one year",
"setGenAuditPasswordAgeYears": "Show warnings for passwords older than {} years",
"setFilePath": "File path",
"setFileStorage": "This file is loaded from {}.",

@ -77,36 +77,37 @@ class MenuModel extends Model {
locTitle: 'setGenAppearance',
icon: '0',
page: 'general',
section: 'appearance',
active: true
section: 'appearance'
},
{
locTitle: 'setGenFunction',
icon: '0',
page: 'general',
section: 'function',
active: true
section: 'function'
},
{
locTitle: 'setGenAudit',
icon: '0',
page: 'general',
section: 'audit'
},
{
locTitle: 'setGenLock',
icon: '0',
page: 'general',
section: 'lock',
active: true
section: 'lock'
},
{
locTitle: 'setGenStorage',
icon: '0',
page: 'general',
section: 'storage',
active: true
section: 'storage'
},
{
locTitle: 'advanced',
icon: '0',
page: 'general',
section: 'advanced',
active: true
section: 'advanced'
}
]);
this.shortcutsSection = new MenuSectionModel([

@ -0,0 +1,85 @@
/**
* Password strength level estimation according to OWASP password recommendations and entropy
* https://auth0.com/docs/connections/database/password-strength
*/
const PasswordStrengthLevel = {
None: 0,
Low: 1,
Good: 2
};
const charClasses = new Uint8Array(128);
for (let i = 48 /* '0' */; i <= 57 /* '9' */; i++) {
charClasses[i] = 1;
}
for (let i = 97 /* 'a' */; i <= 122 /* 'z' */; i++) {
charClasses[i] = 2;
}
for (let i = 65 /* 'A' */; i <= 90 /* 'Z' */; i++) {
charClasses[i] = 3;
}
const symbolsPerCharClass = new Uint8Array([
95 /* ASCII symbols */,
10 /* digits */,
26 /* lowercase letters */,
26 /* uppercase letters */
]);
function passwordStrength(password) {
if (!password || !password.isProtected) {
throw new TypeError('Bad password type');
}
if (!password.byteLength) {
return { level: PasswordStrengthLevel.None, length: 0 };
}
let length = 0;
const countByClass = [0, 0, 0, 0];
let isSingleChar = true;
let prevCharCode = -1;
password.forEachChar((charCode) => {
const charClass = charCode < charClasses.length ? charClasses[charCode] : 0;
countByClass[charClass]++;
length++;
if (isSingleChar) {
if (charCode !== prevCharCode) {
if (prevCharCode === -1) {
prevCharCode = charCode;
} else {
isSingleChar = false;
}
}
}
});
const onlyDigits = countByClass[1] === length;
if (length < 6) {
return { level: PasswordStrengthLevel.None, length, onlyDigits };
}
if (isSingleChar) {
return { level: PasswordStrengthLevel.None, length, onlyDigits };
}
if (length < 8) {
return { level: PasswordStrengthLevel.Low, length, onlyDigits };
}
let alphabetSize = 0;
for (let i = 0; i < countByClass.length; i++) {
if (countByClass[i] > 0) {
alphabetSize += symbolsPerCharClass[i];
}
}
const entropy = Math.log2(Math.pow(alphabetSize, length));
const level = entropy < 60 ? PasswordStrengthLevel.Low : PasswordStrengthLevel.Good;
return { length, level, onlyDigits };
}
export { PasswordStrengthLevel, passwordStrength };

@ -139,8 +139,6 @@ kdbxweb.ProtectedValue.prototype.indexOfSelfInLower = function (targetLower) {
return firstCharIndex;
};
window.PV = kdbxweb.ProtectedValue;
kdbxweb.ProtectedValue.prototype.equals = function (other) {
if (!other) {
return false;
@ -176,3 +174,19 @@ kdbxweb.ProtectedValue.prototype.isFieldReference = function () {
});
return true;
};
const RandomSalt = kdbxweb.Random.getBytes(128);
kdbxweb.ProtectedValue.prototype.saltedValue = function () {
if (!this.byteLength) {
return 0;
}
const value = this._value;
const salt = this._salt;
let salted = '';
for (let i = 0, len = value.length; i < len; i++) {
const byte = value[i] ^ salt[i];
salted += String.fromCharCode(byte ^ RandomSalt[i % RandomSalt.length]);
}
return salted;
};

@ -0,0 +1,122 @@
import { View } from 'framework/views/view';
import template from 'templates/details/details-issues.hbs';
import { Alerts } from 'comp/ui/alerts';
import { Timeouts } from 'const/timeouts';
import { passwordStrength, PasswordStrengthLevel } from 'util/data/password-strength';
import { AppSettingsModel } from 'models/app-settings-model';
import { Links } from 'const/links';
import { checkIfPasswordIsExposedOnline } from 'comp/app/online-password-checker';
class DetailsIssuesView extends View {
parent = '.details__issues-container';
template = template;
events = {
'click .details__issues-close-btn': 'closeIssuesClick'
};
passwordIssue = null;
constructor(model) {
super(model);
this.listenTo(AppSettingsModel, 'change', this.settingsChanged);
if (AppSettingsModel.auditPasswords) {
this.checkPasswordIssues();
}
}
render(options) {
if (!AppSettingsModel.auditPasswords) {
super.render();
return;
}
super.render({
hibpLink: Links.HaveIBeenPwned,
passwordIssue: this.passwordIssue,
fadeIn: options?.fadeIn
});
}
settingsChanged() {
if (AppSettingsModel.auditPasswords) {
this.checkPasswordIssues();
}
this.render();
}
passwordChanged() {
const oldPasswordIssue = this.passwordIssue;
this.checkPasswordIssues();
if (oldPasswordIssue !== this.passwordIssue) {
const fadeIn = !oldPasswordIssue;
if (this.passwordIssue) {
this.render({ fadeIn });
} else {
this.el.classList.add('fade-out');
setTimeout(() => this.render(), Timeouts.FastAnimation);
}
}
}
checkPasswordIssues() {
const { password } = this.model;
if (!password || !password.isProtected || !password.byteLength) {
this.passwordIssue = null;
return;
}
const strength = passwordStrength(password);
if (AppSettingsModel.excludePinsFromAudit && strength.onlyDigits && strength.length <= 6) {
this.passwordIssue = null;
} else if (strength.level < PasswordStrengthLevel.Low) {
this.passwordIssue = 'poor';
} else if (strength.level < PasswordStrengthLevel.Good) {
this.passwordIssue = 'weak';
} else if (AppSettingsModel.auditPasswordAge && this.isOld()) {
this.passwordIssue = 'old';
} else {
this.passwordIssue = null;
this.checkOnHIBP();
}
}
isOld() {
if (!this.model.updated) {
return false;
}
const dt = new Date(this.model.updated);
dt.setFullYear(dt.getFullYear() + AppSettingsModel.auditPasswordAge);
return dt < Date.now();
}
checkOnHIBP() {
if (!AppSettingsModel.checkPasswordsOnHIBP) {
return;
}
const isExposed = checkIfPasswordIsExposedOnline(this.model.password);
if (typeof isExposed === 'boolean') {
this.passwordIssue = isExposed ? 'pwned' : null;
} else {
const iconEl = this.el?.querySelector('.details__issues-icon');
iconEl?.classList.add('details__issues-icon--loading');
isExposed.then((isExposed) => {
if (isExposed) {
this.passwordIssue = 'pwned';
} else if (isExposed === false) {
if (this.passwordIssue === 'pwned') {
this.passwordIssue = null;
}
} else {
this.passwordIssue = iconEl ? 'error' : null;
}
this.render();
});
}
}
closeIssuesClick() {
Alerts.notImplemented();
}
}
export { DetailsIssuesView };

@ -20,6 +20,7 @@ import { DetailsAddFieldView } from 'views/details/details-add-field-view';
import { DetailsAttachmentView } from 'views/details/details-attachment-view';
import { DetailsAutoTypeView } from 'views/details/details-auto-type-view';
import { DetailsHistoryView } from 'views/details/details-history-view';
import { DetailsIssuesView } from 'views/details/details-issues-view';
import { DropdownView } from 'views/dropdown-view';
import { createDetailsFields } from 'views/details/details-fields';
import { FieldViewCustom } from 'views/fields/field-view-custom';
@ -117,11 +118,15 @@ class DetailsView extends View {
super.render();
return;
}
const model = { deleted: this.appModel.filter.trash, ...this.model };
const model = {
deleted: this.appModel.filter.trash,
...this.model
};
this.template = template;
super.render(model);
this.setSelectedColor(this.model.color);
this.addFieldViews();
this.checkPasswordIssues();
this.createScroll({
root: this.$el.find('.details__body')[0],
scroller: this.$el.find('.scroller')[0],
@ -576,6 +581,9 @@ class DetailsView extends View {
} else if (fieldName) {
this.model.setField(fieldName, e.val);
}
if (fieldName === 'Password' && this.views.issues) {
this.views.issues.passwordChanged();
}
} else if (e.field === 'Tags') {
this.model.setTags(e.val);
this.appModel.updateTags();
@ -988,6 +996,13 @@ class DetailsView extends View {
Events.emit('auto-type', { entry, sequence });
}
}
checkPasswordIssues() {
if (!this.model.readOnly) {
this.views.issues = new DetailsIssuesView(this.model);
this.views.issues.render();
}
}
}
Object.assign(DetailsView.prototype, Scrollable);

@ -36,6 +36,11 @@ class SettingsGeneralView extends View {
'change .settings__general-auto-save-interval': 'changeAutoSaveInterval',
'change .settings__general-remember-key-files': 'changeRememberKeyFiles',
'change .settings__general-minimize': 'changeMinimize',
'change .settings__general-audit-passwords': 'changeAuditPasswords',
'change .settings__general-exclude-pins-from-audit': 'changeExcludePinsFromAudit',
'change .settings__general-check-passwords-on-hibp': 'changeCheckPasswordsOnHIBP',
'click .settings__general-toggle-help-hibp': 'clickToggleHelpHIBP',
'change .settings__general-audit-password-age': 'changeAuditPasswordAge',
'change .settings__general-lock-on-minimize': 'changeLockOnMinimize',
'change .settings__general-lock-on-copy': 'changeLockOnCopy',
'change .settings__general-lock-on-auto-type': 'changeLockOnAutoType',
@ -97,6 +102,12 @@ class SettingsGeneralView extends View {
canDetectMinimize: !!Launcher,
canDetectOsSleep: Launcher && Launcher.canDetectOsSleep(),
canAutoType: AutoType.enabled,
auditPasswords: AppSettingsModel.auditPasswords,
excludePinsFromAudit: AppSettingsModel.excludePinsFromAudit,
checkPasswordsOnHIBP: AppSettingsModel.checkPasswordsOnHIBP,
auditPasswordAge: AppSettingsModel.auditPasswordAge,
hibpLink: Links.HaveIBeenPwned,
hibpPrivacyLink: Links.HaveIBeenPwnedPrivacy,
lockOnMinimize: Launcher && AppSettingsModel.lockOnMinimize,
lockOnCopy: AppSettingsModel.lockOnCopy,
lockOnAutoType: AppSettingsModel.lockOnAutoType,
@ -320,6 +331,33 @@ class SettingsGeneralView extends View {
AppSettingsModel.minimizeOnClose = minimizeOnClose;
}
changeAuditPasswords(e) {
const auditPasswords = e.target.checked || false;
AppSettingsModel.auditPasswords = auditPasswords;
}
changeExcludePinsFromAudit(e) {
const excludePinsFromAudit = e.target.checked || false;
AppSettingsModel.excludePinsFromAudit = excludePinsFromAudit;
}
changeCheckPasswordsOnHIBP(e) {
if (e.target.closest('a')) {
return;
}
const checkPasswordsOnHIBP = e.target.checked || false;
AppSettingsModel.checkPasswordsOnHIBP = checkPasswordsOnHIBP;
}
clickToggleHelpHIBP() {
this.el.querySelector('.settings__general-help-hibp').classList.toggle('hide');
}
changeAuditPasswordAge(e) {
const auditPasswordAge = e.target.value | 0;
AppSettingsModel.auditPasswordAge = auditPasswordAge;
}
changeLockOnMinimize(e) {
const lockOnMinimize = e.target.checked || false;
AppSettingsModel.lockOnMinimize = lockOnMinimize;

@ -575,6 +575,47 @@
}
}
&__issues {
margin-top: $base-padding-v;
color: var(--text-contrast-error-color);
background-color: var(--error-color);
border-radius: var(--block-border-radius);
display: flex;
align-items: stretch;
flex-direction: row;
justify-content: flex-start;
&-body {
padding: $medium-padding-v 0;
flex-grow: 1;
}
&-icon {
padding: $medium-padding;
width: 1em;
&-spin {
display: none;
.details__issues-icon--loading & {
display: inline-block;
}
}
&-warning {
.details__issues-icon--loading & {
display: none;
}
}
}
&-close-btn {
padding: $medium-padding;
cursor: pointer;
align-self: flex-start;
opacity: 0.8;
transition: opacity $base-duration $base-timing;
&:hover {
opacity: 1;
}
}
}
&__buttons {
display: flex;
align-items: stretch;

@ -66,9 +66,12 @@ $titlebar-padding-large: 40px;
// Animations
$base-duration: 150ms;
$fast-duration: 80ms;
$base-timing: ease;
$slow-transition-in: $base-duration * 2 ease-in;
$slow-transition-out: $base-duration ease-out;
$fast-transition-in: $fast-duration ease-in;
$fast-transition-out: $fast-duration ease-out;
$tip-transition-in: 500ms $ease-in-expo;
$tip-transition-out: $slow-transition-out;

@ -25,6 +25,15 @@
animation: shake 50s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0s;
}
.fade-in {
animation: fade-in $fast-transition-in 0s;
}
.fade-out {
opacity: 0;
animation: fade-out $fast-transition-out 0s;
}
.rotate-90,
.fa.rotate-90:before {
transform: rotate(90deg);
@ -52,6 +61,24 @@
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes shake {
0%,
1%,

@ -1,6 +1,10 @@
.info-btn {
cursor: pointer;
color: var(--muted-color);
margin-left: $tiny-spacing;
position: relative;
top: 0.15em;
font-size: 1.1em;
&:hover {
color: var(--text-color);
}

@ -0,0 +1,28 @@
{{#if passwordIssue}}
<div class="details__issues {{#if fadeIn}}fade-in{{/if}}">
<div class="details__issues-icon">
<i class="fa fa-exclamation-triangle details__issues-icon-warning"></i>
<i class="fa fa-spinner spin details__issues-icon-spin"></i>
</div>
<div class="details__issues-body">
{{#ifeq passwordIssue 'weak'}}
{{~res 'detIssueWeakPassword'~}}
{{else ifeq passwordIssue 'poor'}}
{{~res 'detIssuePoorPassword'~}}
{{else ifeq passwordIssue 'pwned'}}
{{~#res 'detIssuePwnedPassword'~}}
<a href="{{hibpLink}}" rel="noreferrer noopener" target="_blank">Have I Been Pwned</a>
{{~/res~}}
{{else ifeq passwordIssue 'old'}}
{{~res 'detIssueOldPassword'~}}
{{else ifeq passwordIssue 'error'}}
{{~res 'detIssuePasswordCheckError'~}}
{{/ifeq}}
</div>
<div class="details__issues-close-btn" title="{{res 'detIssuesHideTooltip'}}">
<i class="fa fa-times-circle"></i>
</div>
</div>
{{else}}
<div></div>
{{/if}}

@ -43,6 +43,8 @@
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
</div>
{{#unless readOnly}}
<div class="details__issues-container">
</div>
<div class="details__buttons">
{{#if deleted~}}
<i class="details__buttons-trash-del fa fa-minus-circle" title="{{res 'detDelEntryPerm'}}" tip-placement="top"></i>

@ -177,6 +177,52 @@
<label for="settings__general-use-group-icon-for-entries">{{res 'setGenUseGroupIconForEntries'}}</label>
</div>
<h2 id="audit">{{res 'setGenAudit'}}</h2>
<div>
<input type="checkbox" class="settings__input input-base settings__general-audit-passwords"
id="settings__general-audit-passwords" {{#if auditPasswords}}checked{{/if}} />
<label for="settings__general-audit-passwords">{{res 'setGenAuditPasswords'}}</label>
</div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-exclude-pins-from-audit"
id="settings__general-exclude-pins-from-audit" {{#if excludePinsFromAudit}}checked{{/if}} />
<label for="settings__general-exclude-pins-from-audit">{{res 'setGenExcludePinsFromAudit'}}</label>
</div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-check-passwords-on-hibp"
id="settings__general-check-passwords-on-hibp" {{#if checkPasswordsOnHIBP}}checked{{/if}} />
<label for="settings__general-check-passwords-on-hibp">
{{~#res 'setGenCheckPasswordsOnHIBP'~}}
<a href="{{hibpLink}}" rel="noreferrer noopener" target="_blank">Have I Been Pwned</a>
{{~/res~}}
</label>
<i class="fa fa-info-circle info-btn settings__general-toggle-help-hibp"></i>
<div class="settings__general-help-hibp hide">
{{~#res 'setGenHelpHIBP'~}}
<a href="{{hibpPrivacyLink}}" rel="noreferrer noopener" target="_blank">{{res 'setGenHelpHIBPLink'}}</a>
{{~/res~}}
</div>
</div>
<div>
<label for="settings__general-audit-password-age">{{res 'setGenAuditPasswordAge'}}:</label>
<select class="settings__select input-base settings__general-audit-password-age"
id="settings__general-audit-password-age">
<option value="0" {{#ifeq auditPasswordAge 0}}selected{{/ifeq}}>{{res 'setGenAuditPasswordAgeOff'}}</option>
<option value="1" {{#ifeq auditPasswordAge 1}}selected{{/ifeq}}>{{res 'setGenAuditPasswordAgeOneYear'}}</option>
<option value="2" {{#ifeq auditPasswordAge 2}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
2{{/res}}</option>
<option value="3" {{#ifeq auditPasswordAge 3}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
3{{/res}}</option>
<option value="5" {{#ifeq auditPasswordAge 5}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
5{{/res}}</option>
<option value="10" {{#ifeq auditPasswordAge 10}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
10{{/res}}</option>
</select>
</div>
<h2 id="lock">{{res 'setGenLock'}}</h2>
<div>
<label for="settings__general-idle-minutes">{{res 'setGenLockInactive'}}:</label>

@ -1,10 +1,12 @@
Release notes
-------------
##### v1.17.0 (TBD)
`+` password quality warnings
`+` "Have I Been Pwned" service integration (opt-in)
`+` automatically switching between dark and light theme
`+` clear searchbox button
`+` favicon download improvements
`+` auto-type field selection dropdown improvements
`+` auto-type field selection dropdown improvements
##### v1.16.5 (2020-12-18)
`-` using custom OneDrive without a secret

@ -0,0 +1,52 @@
import { expect } from 'chai';
import { ProtectedValue } from 'kdbxweb';
import { PasswordStrengthLevel, passwordStrength } from 'util/data/password-strength';
describe('PasswordStrength', () => {
function check(password, expected) {
let actual = passwordStrength(ProtectedValue.fromString(password));
expected = { onlyDigits: false, ...expected };
actual = { onlyDigits: false, ...actual };
for (const [prop, expVal] of Object.entries(expected)) {
expect(actual[prop]).to.eql(expVal, `${prop} is ${expVal} for password "${password}"`);
}
}
it('should throw an error for non-passwords', () => {
expect(() => passwordStrength('')).to.throw(TypeError);
expect(() => passwordStrength(null)).to.throw(TypeError);
});
it('should return level None for short passwords', () => {
check('', { level: PasswordStrengthLevel.None, length: 0 });
check('1234', { level: PasswordStrengthLevel.None, length: 4, onlyDigits: true });
});
it('should return level None for single character passwords', () => {
check('000000000000', { level: PasswordStrengthLevel.None, length: 12, onlyDigits: true });
});
it('should return level Low for simple passwords', () => {
check('12345=', { level: PasswordStrengthLevel.Low, length: 6 });
check('12345Aa', { level: PasswordStrengthLevel.Low, length: 7 });
check('1234567a', { level: PasswordStrengthLevel.Low, length: 8 });
check('1234567ab', { level: PasswordStrengthLevel.Low, length: 9 });
check('1234Ab', { level: PasswordStrengthLevel.Low, length: 6 });
check('1234567', { level: PasswordStrengthLevel.Low, length: 7, onlyDigits: true });
check('123456789012345678', { level: PasswordStrengthLevel.Low, onlyDigits: true });
check('abcdefghijkl', { level: PasswordStrengthLevel.Low });
});
it('should return level Good for passwords matching all criteria', () => {
check('123456ABcdef', { level: PasswordStrengthLevel.Good, length: 12 });
check('Abcdef=5k', { level: PasswordStrengthLevel.Good, length: 9 });
check('12345678901234567890123456', {
level: PasswordStrengthLevel.Good,
onlyDigits: true
});
});
it('should work with long passwords', () => {
check('ABCDabcd_-+=' + '1234567890'.repeat(100), { level: PasswordStrengthLevel.Good });
});
});
Loading…
Cancel
Save