dsh-vault 1.10.71 → 1.10.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh.md +32 -9
- package/README.md +45 -11
- package/lib/breach.js +10 -3
- package/lib/client.js +168 -168
- package/lib/index.js +12 -53
- package/lib/password-strength.js +170 -0
- package/package.json +1 -1
package/README-zh.md
CHANGED
|
@@ -324,19 +324,42 @@ npx dsh-vault list # 或者干脆不装
|
|
|
324
324
|
|
|
325
325
|
profile 这条路还有两个注意点:垫片可能被之后在该目录执行的 `pnpm install` 清掉;`pnpm dsh plugin --profile web add dsh-vault` 会依据包的 `dsh.bundle` 声明重整 `dsh.profile.bundles` —— 如果你自己的 patch 层**也**插入了这个插件,就会**挂两份**,要先删掉 patch 层那一行。
|
|
326
326
|
|
|
327
|
+
## 泄露检查的隐私边界
|
|
328
|
+
|
|
329
|
+
`vault_breach_check`(以及安全页)可以把密码拿去和 Have I Been Pwned 比对。**离开本机的只有一样东西**:
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
GET https://api.pwnedpasswords.com/range/<SHA-1(密码) 的前 5 个十六进制字符>
|
|
333
|
+
Add-Padding: true
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
响应会返回该前缀桶里的**全部**后缀(几百条哈希),匹配在**本地**完成 —— 完整哈希、密码原文、以及它属于哪个条目都不会离开。如果密码已经在内置常见口令表里,**根本不会发起请求**;同一前缀的重复查询会在内存里缓存一小时。
|
|
337
|
+
|
|
338
|
+
k-anonymity **没有**隐藏的东西,直说:
|
|
339
|
+
|
|
340
|
+
- **5 个字符的前缀**(20 bit,约百万分之一桶)会连同你的 IP 发给 Cloudflare;
|
|
341
|
+
- 能枚举候选口令的观察者,可以算出每个候选的前缀,从而看出你是否查询过那个桶 —— 这是该协议的已知局限,加请求头也解决不了;
|
|
342
|
+
- `Add-Padding` 只是抹掉**响应体积**这个信号(不加它,桶的响应长度会和"该密码有多常见"相关)。
|
|
343
|
+
|
|
344
|
+
如果连前缀都不想出去,就用 `online: false` —— 此时只查离线词表,覆盖率小得多,适合当筛子而不是定论。
|
|
345
|
+
|
|
327
346
|
## 强度指示
|
|
328
347
|
|
|
329
|
-
列表里每个条目在标题后有一个**三星**强度指示 —— **☆☆☆ = 0 分,★★★ =
|
|
348
|
+
列表里每个条目在标题后有一个**三星**强度指示 —— **☆☆☆ = 0 分,★★★ = 满分**,中间按半星分档:
|
|
349
|
+
|
|
350
|
+

|
|
351
|
+
|
|
352
|
+
半星是**真正的左半实心、右半空心**,不是在旁边写个 `½`:实现是把同样三颗星叠两层、按百分比裁剪彩色层,而 `★`/`☆` 字宽一致,所以裁剪正好落在星的边界上。
|
|
330
353
|
|
|
331
|
-
| 分数 |
|
|
354
|
+
| 分数 | 实心星数 | 档位 |
|
|
332
355
|
|---|---|---|
|
|
333
|
-
| 0–8 |
|
|
334
|
-
| 9–24 |
|
|
335
|
-
| 25–41 |
|
|
336
|
-
| 42–58 |
|
|
337
|
-
| 59–74 |
|
|
338
|
-
| 75–91 |
|
|
339
|
-
| 92–100 |
|
|
356
|
+
| 0–8 | 0 | 弱 |
|
|
357
|
+
| 9–24 | ½ | 弱 |
|
|
358
|
+
| 25–41 | 1 | 弱 |
|
|
359
|
+
| 42–58 | 1½ | 中 |
|
|
360
|
+
| 59–74 | 2 | 中 |
|
|
361
|
+
| 75–91 | 2½ | 强 |
|
|
362
|
+
| 92–100 | 3 | 很强 |
|
|
340
363
|
|
|
341
364
|
悬停可看到精确分数与评价。分数在 **host 侧**计算(列表本身永远拿不到密钥),针对条目的**密码或银行卡 PIN**;机器生成的 API Key、私钥**故意不评分**,免得所有密钥都是满分。
|
|
342
365
|
|
package/README.md
CHANGED
|
@@ -359,22 +359,56 @@ Two more caveats on the profile route: its `.bin` shim can be pruned by a later
|
|
|
359
359
|
patch layer *also* inserts the plugin, that mounts it twice, so remove the patch
|
|
360
360
|
row first.
|
|
361
361
|
|
|
362
|
+
## Privacy of the breach check
|
|
363
|
+
|
|
364
|
+
`vault_breach_check` (and the security page) can check passwords against Have I
|
|
365
|
+
Been Pwned. Exactly one thing leaves this machine:
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
GET https://api.pwnedpasswords.com/range/<FIRST 5 HEX CHARS OF SHA-1(password)>
|
|
369
|
+
Add-Padding: true
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The response lists every suffix in that bucket (hundreds of hashes) and the match
|
|
373
|
+
is computed **locally** — the full hash, the password, and the entry it belongs to
|
|
374
|
+
never leave. A password already in the bundled common-password list is answered
|
|
375
|
+
offline and produces **no request at all**; repeat lookups of the same prefix are
|
|
376
|
+
cached in memory for an hour.
|
|
377
|
+
|
|
378
|
+
What k-anonymity does *not* hide, stated plainly:
|
|
379
|
+
|
|
380
|
+
- the **five-character prefix** (20 bits, one bucket in ~1M) goes to Cloudflare,
|
|
381
|
+
together with your IP;
|
|
382
|
+
- an observer who can guess candidate passwords can compute each candidate's
|
|
383
|
+
prefix and see whether you queried that bucket — that is the known limit of
|
|
384
|
+
this protocol, not something the header fixes;
|
|
385
|
+
- `Add-Padding` only removes the *response-size* signal (without it, a bucket's
|
|
386
|
+
response length correlates with how common the password is).
|
|
387
|
+
|
|
388
|
+
To keep even the prefix on your machine, pass `online: false` — the check then
|
|
389
|
+
uses only the offline list, which is far smaller, so treat it as a screening aid
|
|
390
|
+
rather than a verdict.
|
|
391
|
+
|
|
362
392
|
## Strength indicator
|
|
363
393
|
|
|
364
394
|
Each entry row shows a three-star indicator after its title — **☆☆☆ = 0 score,
|
|
365
|
-
★★★ = full marks** — in six half-star steps
|
|
366
|
-
|
|
367
|
-
|
|
395
|
+
★★★ = full marks** — in six half-star steps:
|
|
396
|
+
|
|
397
|
+

|
|
398
|
+
|
|
399
|
+
A half star is a real half-filled star, not a `½` next to it: the coloured copy
|
|
400
|
+
of the same three glyphs is clipped to a percentage, and `★`/`☆` share an advance
|
|
401
|
+
width so the clip lands exactly on a star boundary.
|
|
368
402
|
|
|
369
|
-
| score |
|
|
403
|
+
| score | filled stars | band |
|
|
370
404
|
|---|---|---|
|
|
371
|
-
| 0–8 |
|
|
372
|
-
| 9–24 |
|
|
373
|
-
| 25–41 |
|
|
374
|
-
| 42–58 |
|
|
375
|
-
| 59–74 |
|
|
376
|
-
| 75–91 |
|
|
377
|
-
| 92–100 |
|
|
405
|
+
| 0–8 | 0 | weak |
|
|
406
|
+
| 9–24 | ½ | weak |
|
|
407
|
+
| 25–41 | 1 | weak |
|
|
408
|
+
| 42–58 | 1½ | fair |
|
|
409
|
+
| 59–74 | 2 | fair |
|
|
410
|
+
| 75–91 | 2½ | strong |
|
|
411
|
+
| 92–100 | 3 | very strong |
|
|
378
412
|
|
|
379
413
|
Hovering it shows the exact score and verdict. The score is computed **host-side**
|
|
380
414
|
(the list never receives the secret) and covers the entry's **password or card
|
package/lib/breach.js
CHANGED
|
@@ -17,8 +17,9 @@ const HIBP_RANGE_URL = 'https://api.pwnedpasswords.com/range/';
|
|
|
17
17
|
const QUERY_TIMEOUT_MS = 12_000;
|
|
18
18
|
/** How long a fetched prefix range is cached in memory. */
|
|
19
19
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
20
|
-
/** Common / weak passwords checked offline (rockyou-style head).
|
|
21
|
-
|
|
20
|
+
/** Common / weak passwords checked offline (rockyou-style head). Also used by
|
|
21
|
+
* the strength estimator, so both agree on what "known-common" means. */
|
|
22
|
+
export const WEAK_PASSWORDS = new Set([
|
|
22
23
|
'123456', 'password', '12345678', 'qwerty', '123456789', '12345', '1234', '111111',
|
|
23
24
|
'1234567', 'dragon', '123123', 'baseball', 'abc123', 'football', 'monkey', 'letmein',
|
|
24
25
|
'shadow', 'master', '666666', 'qwertyuiop', '123321', 'mustang', '1234567890', 'michael',
|
|
@@ -42,7 +43,13 @@ export function sha1Hex(password) {
|
|
|
42
43
|
async function queryRange(prefix) {
|
|
43
44
|
const res = await fetch(`${HIBP_RANGE_URL}${prefix}`, {
|
|
44
45
|
signal: AbortSignal.timeout(QUERY_TIMEOUT_MS),
|
|
45
|
-
headers: {
|
|
46
|
+
headers: {
|
|
47
|
+
'User-Agent': 'dsh-vault',
|
|
48
|
+
// HIBP's own privacy knob: pad every response to a similar size so an
|
|
49
|
+
// observer cannot correlate a lookup with how many passwords share that
|
|
50
|
+
// prefix (which would leak information about how common yours is).
|
|
51
|
+
'Add-Padding': 'true',
|
|
52
|
+
},
|
|
46
53
|
});
|
|
47
54
|
if (!res.ok)
|
|
48
55
|
throw new Error(`Pwned Passwords returned HTTP ${res.status}`);
|
package/lib/client.js
CHANGED
|
@@ -510,197 +510,197 @@ window.__ModuleLoader__.load({
|
|
|
510
510
|
document.head.appendChild(tag);
|
|
511
511
|
}
|
|
512
512
|
var VaultSection_module_css_default = {
|
|
513
|
-
"
|
|
514
|
-
"
|
|
515
|
-
"
|
|
516
|
-
"
|
|
513
|
+
"detailBox": "t_j5va_detailBox",
|
|
514
|
+
"secretField": "t_j5va_secretField",
|
|
515
|
+
"pwBar": "t_j5va_pwBar",
|
|
516
|
+
"customKeyInput": "t_j5va_customKeyInput",
|
|
517
|
+
"riskDanger": "t_j5va_riskDanger",
|
|
518
|
+
"strengthStars": "t_j5va_strengthStars",
|
|
519
|
+
"pwWeak": "t_j5va_pwWeak",
|
|
520
|
+
"importActions": "t_j5va_importActions",
|
|
521
|
+
"genOpts": "t_j5va_genOpts",
|
|
522
|
+
"vaultTarget": "t_j5va_vaultTarget",
|
|
517
523
|
"vaultManageRow": "t_j5va_vaultManageRow",
|
|
524
|
+
"importName": "t_j5va_importName",
|
|
525
|
+
"iconFieldWrap": "t_j5va_iconFieldWrap",
|
|
526
|
+
"copied": "t_j5va_copied",
|
|
527
|
+
"field": "t_j5va_field",
|
|
528
|
+
"rowExpired": "t_j5va_rowExpired",
|
|
529
|
+
"dangerButton": "t_j5va_dangerButton",
|
|
530
|
+
"auditChips": "t_j5va_auditChips",
|
|
531
|
+
"reportTitle": "t_j5va_reportTitle",
|
|
518
532
|
"healthBar": "t_j5va_healthBar",
|
|
519
|
-
"
|
|
520
|
-
"
|
|
521
|
-
"
|
|
522
|
-
"
|
|
523
|
-
"
|
|
524
|
-
"totpCopy": "t_j5va_totpCopy",
|
|
525
|
-
"attachBox": "t_j5va_attachBox",
|
|
533
|
+
"pwStrong": "t_j5va_pwStrong",
|
|
534
|
+
"tagName": "t_j5va_tagName",
|
|
535
|
+
"healthSummary": "t_j5va_healthSummary",
|
|
536
|
+
"lenChip": "t_j5va_lenChip",
|
|
537
|
+
"genOptRow": "t_j5va_genOptRow",
|
|
526
538
|
"exportChips": "t_j5va_exportChips",
|
|
527
|
-
"
|
|
528
|
-
"
|
|
529
|
-
"gridDetail": "t_j5va_gridDetail",
|
|
530
|
-
"toolbar": "t_j5va_toolbar",
|
|
539
|
+
"riskBanner": "t_j5va_riskBanner",
|
|
540
|
+
"searchBox": "t_j5va_searchBox",
|
|
531
541
|
"list": "t_j5va_list",
|
|
532
|
-
"
|
|
542
|
+
"moreButton": "t_j5va_moreButton",
|
|
543
|
+
"genHist": "t_j5va_genHist",
|
|
544
|
+
"reportLine": "t_j5va_reportLine",
|
|
545
|
+
"kindChip": "t_j5va_kindChip",
|
|
546
|
+
"customEq": "t_j5va_customEq",
|
|
547
|
+
"detailActions": "t_j5va_detailActions",
|
|
548
|
+
"identity": "t_j5va_identity",
|
|
549
|
+
"auditRow": "t_j5va_auditRow",
|
|
550
|
+
"toolbarDivider": "t_j5va_toolbarDivider",
|
|
551
|
+
"bulkCount": "t_j5va_bulkCount",
|
|
552
|
+
"histAdd": "t_j5va_histAdd",
|
|
553
|
+
"pinOff": "t_j5va_pinOff",
|
|
554
|
+
"brandGlyph": "t_j5va_brandGlyph",
|
|
555
|
+
"detailItem": "t_j5va_detailItem",
|
|
556
|
+
"empty": "t_j5va_empty",
|
|
557
|
+
"kindIcon": "t_j5va_kindIcon",
|
|
558
|
+
"auditWhen": "t_j5va_auditWhen",
|
|
559
|
+
"auditChipActive": "t_j5va_auditChipActive",
|
|
560
|
+
"badge": "t_j5va_badge",
|
|
561
|
+
"rowCheck": "t_j5va_rowCheck",
|
|
562
|
+
"riskWarn": "t_j5va_riskWarn",
|
|
563
|
+
"attachAdd": "t_j5va_attachAdd",
|
|
564
|
+
"tagRow": "t_j5va_tagRow",
|
|
565
|
+
"importList": "t_j5va_importList",
|
|
566
|
+
"emptyHint": "t_j5va_emptyHint",
|
|
533
567
|
"detailDeleteBtn": "t_j5va_detailDeleteBtn",
|
|
534
|
-
"
|
|
535
|
-
"
|
|
568
|
+
"gridRow": "t_j5va_gridRow",
|
|
569
|
+
"policyBar": "t_j5va_policyBar",
|
|
536
570
|
"addButton": "t_j5va_addButton",
|
|
537
|
-
"
|
|
538
|
-
"
|
|
539
|
-
"
|
|
540
|
-
"
|
|
541
|
-
"pwBar": "t_j5va_pwBar",
|
|
542
|
-
"tagName": "t_j5va_tagName",
|
|
543
|
-
"badge": "t_j5va_badge",
|
|
544
|
-
"importActions": "t_j5va_importActions",
|
|
545
|
-
"lenChipActive": "t_j5va_lenChipActive",
|
|
546
|
-
"toolbarDivider": "t_j5va_toolbarDivider",
|
|
547
|
-
"riskDanger": "t_j5va_riskDanger",
|
|
548
|
-
"badgeBtn": "t_j5va_badgeBtn",
|
|
549
|
-
"histDanger": "t_j5va_histDanger",
|
|
550
|
-
"emptyBox": "t_j5va_emptyBox",
|
|
571
|
+
"editor": "t_j5va_editor",
|
|
572
|
+
"genHistItem": "t_j5va_genHistItem",
|
|
573
|
+
"totp": "t_j5va_totp",
|
|
574
|
+
"badgeOk": "t_j5va_badgeOk",
|
|
551
575
|
"tabPane": "t_j5va_tabPane",
|
|
552
|
-
"
|
|
553
|
-
"brandGlyphSm": "t_j5va_brandGlyphSm",
|
|
554
|
-
"strengthStars": "t_j5va_strengthStars",
|
|
555
|
-
"importDesc": "t_j5va_importDesc",
|
|
556
|
-
"actionBtn": "t_j5va_actionBtn",
|
|
557
|
-
"pwStrong": "t_j5va_pwStrong",
|
|
558
|
-
"recentChip": "t_j5va_recentChip",
|
|
559
|
-
"rowSelected": "t_j5va_rowSelected",
|
|
560
|
-
"tabs": "t_j5va_tabs",
|
|
576
|
+
"recentClear": "t_j5va_recentClear",
|
|
561
577
|
"kindChipActive": "t_j5va_kindChipActive",
|
|
562
|
-
"
|
|
563
|
-
"identity": "t_j5va_identity",
|
|
564
|
-
"actionPrimary": "t_j5va_actionPrimary",
|
|
565
|
-
"pwHistRow": "t_j5va_pwHistRow",
|
|
566
|
-
"genHistItem": "t_j5va_genHistItem",
|
|
567
|
-
"editorActions": "t_j5va_editorActions",
|
|
568
|
-
"dangerButton": "t_j5va_dangerButton",
|
|
569
|
-
"histNeutral": "t_j5va_histNeutral",
|
|
578
|
+
"row": "t_j5va_row",
|
|
570
579
|
"kindFilter": "t_j5va_kindFilter",
|
|
571
|
-
"
|
|
572
|
-
"
|
|
573
|
-
"
|
|
574
|
-
"
|
|
575
|
-
"
|
|
576
|
-
"tab": "t_j5va_tab",
|
|
577
|
-
"actionGroup": "t_j5va_actionGroup",
|
|
578
|
-
"kindIcon": "t_j5va_kindIcon",
|
|
579
|
-
"pinStar": "t_j5va_pinStar",
|
|
580
|
-
"backupButton": "t_j5va_backupButton",
|
|
581
|
-
"auditChipActive": "t_j5va_auditChipActive",
|
|
580
|
+
"dupMerge": "t_j5va_dupMerge",
|
|
581
|
+
"brandGlyphSm": "t_j5va_brandGlyphSm",
|
|
582
|
+
"rowDue": "t_j5va_rowDue",
|
|
583
|
+
"tagActions": "t_j5va_tagActions",
|
|
584
|
+
"vaultSwitch": "t_j5va_vaultSwitch",
|
|
582
585
|
"recentLabel": "t_j5va_recentLabel",
|
|
583
586
|
"badgeActive": "t_j5va_badgeActive",
|
|
584
|
-
"
|
|
585
|
-
"
|
|
586
|
-
"
|
|
587
|
-
"
|
|
588
|
-
"
|
|
589
|
-
"genHist": "t_j5va_genHist",
|
|
590
|
-
"scoreRing": "t_j5va_scoreRing",
|
|
591
|
-
"selectActive": "t_j5va_selectActive",
|
|
592
|
-
"auditChips": "t_j5va_auditChips",
|
|
593
|
-
"badgeWarn": "t_j5va_badgeWarn",
|
|
594
|
-
"detailItem": "t_j5va_detailItem",
|
|
595
|
-
"colorSwatch": "t_j5va_colorSwatch",
|
|
596
|
-
"badgeLink": "t_j5va_badgeLink",
|
|
597
|
-
"customEq": "t_j5va_customEq",
|
|
598
|
-
"tagCount": "t_j5va_tagCount",
|
|
599
|
-
"recentSearch": "t_j5va_recentSearch",
|
|
600
|
-
"totp": "t_j5va_totp",
|
|
601
|
-
"revealButton": "t_j5va_revealButton",
|
|
602
|
-
"customFieldRow": "t_j5va_customFieldRow",
|
|
603
|
-
"genOpts": "t_j5va_genOpts",
|
|
604
|
-
"checkField": "t_j5va_checkField",
|
|
605
|
-
"resultCount": "t_j5va_resultCount",
|
|
606
|
-
"pwMeter": "t_j5va_pwMeter",
|
|
607
|
-
"tagManager": "t_j5va_tagManager",
|
|
587
|
+
"gridDetail": "t_j5va_gridDetail",
|
|
588
|
+
"detailCode": "t_j5va_detailCode",
|
|
589
|
+
"actionBtn": "t_j5va_actionBtn",
|
|
590
|
+
"lockedTag": "t_j5va_lockedTag",
|
|
591
|
+
"pinStar": "t_j5va_pinStar",
|
|
608
592
|
"toolbarFilters": "t_j5va_toolbarFilters",
|
|
609
|
-
"
|
|
610
|
-
"
|
|
611
|
-
"
|
|
612
|
-
"headerMessage": "t_j5va_headerMessage",
|
|
613
|
-
"attachAdd": "t_j5va_attachAdd",
|
|
614
|
-
"empty": "t_j5va_empty",
|
|
615
|
-
"field": "t_j5va_field",
|
|
616
|
-
"reportBox": "t_j5va_reportBox",
|
|
617
|
-
"strengthTrack": "t_j5va_strengthTrack",
|
|
618
|
-
"colorFieldWrap": "t_j5va_colorFieldWrap",
|
|
619
|
-
"bulkBar": "t_j5va_bulkBar",
|
|
593
|
+
"modeAuto": "t_j5va_modeAuto",
|
|
594
|
+
"policyField": "t_j5va_policyField",
|
|
595
|
+
"revealButton": "t_j5va_revealButton",
|
|
620
596
|
"modeAsk": "t_j5va_modeAsk",
|
|
597
|
+
"actionGroupLabel": "t_j5va_actionGroupLabel",
|
|
598
|
+
"status": "t_j5va_status",
|
|
599
|
+
"rowMain": "t_j5va_rowMain",
|
|
600
|
+
"pinOn": "t_j5va_pinOn",
|
|
601
|
+
"gridInput": "t_j5va_gridInput",
|
|
621
602
|
"histLink": "t_j5va_histLink",
|
|
622
|
-
"reportTitle": "t_j5va_reportTitle",
|
|
623
|
-
"pwFair": "t_j5va_pwFair",
|
|
624
|
-
"colorPalette": "t_j5va_colorPalette",
|
|
625
|
-
"auditWhen": "t_j5va_auditWhen",
|
|
626
|
-
"editor": "t_j5va_editor",
|
|
627
|
-
"hit": "t_j5va_hit",
|
|
628
|
-
"tagRow": "t_j5va_tagRow",
|
|
629
|
-
"histUpdate": "t_j5va_histUpdate",
|
|
630
|
-
"importList": "t_j5va_importList",
|
|
631
|
-
"emojiBtn": "t_j5va_emojiBtn",
|
|
632
|
-
"iconFieldWrap": "t_j5va_iconFieldWrap",
|
|
633
|
-
"genOptRow": "t_j5va_genOptRow",
|
|
634
603
|
"title": "t_j5va_title",
|
|
635
|
-
"
|
|
636
|
-
"
|
|
637
|
-
"
|
|
638
|
-
"
|
|
639
|
-
"
|
|
640
|
-
"badgeDanger": "t_j5va_badgeDanger",
|
|
641
|
-
"detailCode": "t_j5va_detailCode",
|
|
642
|
-
"copied": "t_j5va_copied",
|
|
643
|
-
"healthSummary": "t_j5va_healthSummary",
|
|
644
|
-
"moreWrap": "t_j5va_moreWrap",
|
|
645
|
-
"intro": "t_j5va_intro",
|
|
646
|
-
"auditChip": "t_j5va_auditChip",
|
|
647
|
-
"policyField": "t_j5va_policyField",
|
|
648
|
-
"dupGroup": "t_j5va_dupGroup",
|
|
649
|
-
"urlGlyph": "t_j5va_urlGlyph",
|
|
604
|
+
"lockedBanner": "t_j5va_lockedBanner",
|
|
605
|
+
"selectActive": "t_j5va_selectActive",
|
|
606
|
+
"backupButton": "t_j5va_backupButton",
|
|
607
|
+
"toolbar": "t_j5va_toolbar",
|
|
608
|
+
"importDesc": "t_j5va_importDesc",
|
|
650
609
|
"auditIssues": "t_j5va_auditIssues",
|
|
651
|
-
"
|
|
652
|
-
"deleteButton": "t_j5va_deleteButton",
|
|
653
|
-
"dupMerge": "t_j5va_dupMerge",
|
|
654
|
-
"favActive": "t_j5va_favActive",
|
|
655
|
-
"copyAllBtn": "t_j5va_copyAllBtn",
|
|
656
|
-
"sortButton": "t_j5va_sortButton",
|
|
657
|
-
"pinOff": "t_j5va_pinOff",
|
|
658
|
-
"dupNames": "t_j5va_dupNames",
|
|
659
|
-
"retryButton": "t_j5va_retryButton",
|
|
660
|
-
"lenChip": "t_j5va_lenChip",
|
|
661
|
-
"customKeyInput": "t_j5va_customKeyInput",
|
|
662
|
-
"actionGroups": "t_j5va_actionGroups",
|
|
663
|
-
"strengthWeak": "t_j5va_strengthWeak",
|
|
664
|
-
"pinOn": "t_j5va_pinOn",
|
|
665
|
-
"rowExpired": "t_j5va_rowExpired",
|
|
666
|
-
"customFieldRows": "t_j5va_customFieldRows",
|
|
667
|
-
"detailBox": "t_j5va_detailBox",
|
|
668
|
-
"riskWarn": "t_j5va_riskWarn",
|
|
669
|
-
"moreButton": "t_j5va_moreButton",
|
|
670
|
-
"brandGlyph": "t_j5va_brandGlyph",
|
|
671
|
-
"recentClear": "t_j5va_recentClear",
|
|
672
|
-
"policyBar": "t_j5va_policyBar",
|
|
673
|
-
"reportSub": "t_j5va_reportSub",
|
|
674
|
-
"badgeOk": "t_j5va_badgeOk",
|
|
675
|
-
"gridRow": "t_j5va_gridRow",
|
|
610
|
+
"editorActions": "t_j5va_editorActions",
|
|
676
611
|
"gridName": "t_j5va_gridName",
|
|
677
|
-
"
|
|
678
|
-
"moreMenu": "t_j5va_moreMenu",
|
|
612
|
+
"uriCode": "t_j5va_uriCode",
|
|
679
613
|
"section": "t_j5va_section",
|
|
680
|
-
"
|
|
681
|
-
"
|
|
682
|
-
"
|
|
683
|
-
"
|
|
614
|
+
"badgeLink": "t_j5va_badgeLink",
|
|
615
|
+
"favActive": "t_j5va_favActive",
|
|
616
|
+
"colorFieldWrap": "t_j5va_colorFieldWrap",
|
|
617
|
+
"detailRemain": "t_j5va_detailRemain",
|
|
618
|
+
"strengthTrack": "t_j5va_strengthTrack",
|
|
619
|
+
"searchKbd": "t_j5va_searchKbd",
|
|
620
|
+
"histUpdate": "t_j5va_histUpdate",
|
|
621
|
+
"actionPrimary": "t_j5va_actionPrimary",
|
|
622
|
+
"dupNames": "t_j5va_dupNames",
|
|
623
|
+
"totpCopy": "t_j5va_totpCopy",
|
|
624
|
+
"pwHistRow": "t_j5va_pwHistRow",
|
|
625
|
+
"badgeDanger": "t_j5va_badgeDanger",
|
|
626
|
+
"toolbarMain": "t_j5va_toolbarMain",
|
|
684
627
|
"strengthStrong": "t_j5va_strengthStrong",
|
|
685
|
-
"
|
|
686
|
-
"
|
|
628
|
+
"auditChip": "t_j5va_auditChip",
|
|
629
|
+
"pwFair": "t_j5va_pwFair",
|
|
630
|
+
"totpRing": "t_j5va_totpRing",
|
|
631
|
+
"tab": "t_j5va_tab",
|
|
632
|
+
"customFieldRows": "t_j5va_customFieldRows",
|
|
633
|
+
"favToggle": "t_j5va_favToggle",
|
|
634
|
+
"emojiBtn": "t_j5va_emojiBtn",
|
|
635
|
+
"tabActive": "t_j5va_tabActive",
|
|
636
|
+
"intro": "t_j5va_intro",
|
|
687
637
|
"pwLabel": "t_j5va_pwLabel",
|
|
688
|
-
"vaultSwitch": "t_j5va_vaultSwitch",
|
|
689
|
-
"reportLine": "t_j5va_reportLine",
|
|
690
|
-
"modeReadonly": "t_j5va_modeReadonly",
|
|
691
|
-
"error": "t_j5va_error",
|
|
692
|
-
"highBadge": "t_j5va_highBadge",
|
|
693
|
-
"bulkCount": "t_j5va_bulkCount",
|
|
694
|
-
"searchKbd": "t_j5va_searchKbd",
|
|
695
|
-
"modeAuto": "t_j5va_modeAuto",
|
|
696
638
|
"trashButton": "t_j5va_trashButton",
|
|
697
|
-
"
|
|
698
|
-
"
|
|
639
|
+
"hit": "t_j5va_hit",
|
|
640
|
+
"histDanger": "t_j5va_histDanger",
|
|
641
|
+
"histNeutral": "t_j5va_histNeutral",
|
|
642
|
+
"editorBody": "t_j5va_editorBody",
|
|
643
|
+
"highBadge": "t_j5va_highBadge",
|
|
699
644
|
"detailTotp": "t_j5va_detailTotp",
|
|
700
|
-
"
|
|
701
|
-
"
|
|
645
|
+
"reportJump": "t_j5va_reportJump",
|
|
646
|
+
"strengthFair": "t_j5va_strengthFair",
|
|
647
|
+
"colorSwatch": "t_j5va_colorSwatch",
|
|
648
|
+
"importRow": "t_j5va_importRow",
|
|
649
|
+
"exportChip": "t_j5va_exportChip",
|
|
650
|
+
"recentSearch": "t_j5va_recentSearch",
|
|
651
|
+
"recentChip": "t_j5va_recentChip",
|
|
652
|
+
"strengthWeak": "t_j5va_strengthWeak",
|
|
653
|
+
"emptyBox": "t_j5va_emptyBox",
|
|
654
|
+
"pwHistPwd": "t_j5va_pwHistPwd",
|
|
655
|
+
"scoreRing": "t_j5va_scoreRing",
|
|
656
|
+
"modeReadonly": "t_j5va_modeReadonly",
|
|
657
|
+
"badgeWarn": "t_j5va_badgeWarn",
|
|
658
|
+
"saveButton": "t_j5va_saveButton",
|
|
659
|
+
"bulkBar": "t_j5va_bulkBar",
|
|
660
|
+
"moreMenu": "t_j5va_moreMenu",
|
|
661
|
+
"dueBadge": "t_j5va_dueBadge",
|
|
662
|
+
"urlGlyph": "t_j5va_urlGlyph",
|
|
663
|
+
"retryButton": "t_j5va_retryButton",
|
|
664
|
+
"lenChips": "t_j5va_lenChips",
|
|
665
|
+
"lenChipActive": "t_j5va_lenChipActive",
|
|
666
|
+
"strengthFill": "t_j5va_strengthFill",
|
|
667
|
+
"tagManager": "t_j5va_tagManager",
|
|
668
|
+
"srOnly": "t_j5va_srOnly",
|
|
669
|
+
"resultCount": "t_j5va_resultCount",
|
|
670
|
+
"attachBox": "t_j5va_attachBox",
|
|
671
|
+
"tabs": "t_j5va_tabs",
|
|
672
|
+
"bulkItem": "t_j5va_bulkItem",
|
|
673
|
+
"reportBox": "t_j5va_reportBox",
|
|
674
|
+
"rowActions": "t_j5va_rowActions",
|
|
702
675
|
"pwHistBox": "t_j5va_pwHistBox",
|
|
703
|
-
"
|
|
676
|
+
"reportLineLink": "t_j5va_reportLineLink",
|
|
677
|
+
"tagCount": "t_j5va_tagCount",
|
|
678
|
+
"actionGroups": "t_j5va_actionGroups",
|
|
679
|
+
"pwMeter": "t_j5va_pwMeter",
|
|
680
|
+
"colorPalette": "t_j5va_colorPalette",
|
|
681
|
+
"copyAllBtn": "t_j5va_copyAllBtn",
|
|
682
|
+
"customValueInput": "t_j5va_customValueInput",
|
|
683
|
+
"inlineInput": "t_j5va_inlineInput",
|
|
684
|
+
"emojiPicker": "t_j5va_emojiPicker",
|
|
685
|
+
"moreWrap": "t_j5va_moreWrap",
|
|
686
|
+
"gridActions": "t_j5va_gridActions",
|
|
687
|
+
"badgeBtn": "t_j5va_badgeBtn",
|
|
688
|
+
"dupGroup": "t_j5va_dupGroup",
|
|
689
|
+
"deleteButton": "t_j5va_deleteButton",
|
|
690
|
+
"pwHistTime": "t_j5va_pwHistTime",
|
|
691
|
+
"sortButton": "t_j5va_sortButton",
|
|
692
|
+
"tagManagerHead": "t_j5va_tagManagerHead",
|
|
693
|
+
"actionGroup": "t_j5va_actionGroup",
|
|
694
|
+
"error": "t_j5va_error",
|
|
695
|
+
"vaultHeader": "t_j5va_vaultHeader",
|
|
696
|
+
"checkField": "t_j5va_checkField",
|
|
697
|
+
"clearButton": "t_j5va_clearButton",
|
|
698
|
+
"headerMessage": "t_j5va_headerMessage",
|
|
699
|
+
"reportSub": "t_j5va_reportSub",
|
|
700
|
+
"rowSelected": "t_j5va_rowSelected",
|
|
701
|
+
"listCompact": "t_j5va_listCompact",
|
|
702
|
+
"customFieldRow": "t_j5va_customFieldRow",
|
|
703
|
+
"inlineField": "t_j5va_inlineField"
|
|
704
704
|
};
|
|
705
705
|
//#endregion
|
|
706
706
|
//#region src/client/VaultSection.tsx
|
package/lib/index.js
CHANGED
|
@@ -62,7 +62,8 @@ import { envLinesFor, shellQuote } from "./env-export.js";
|
|
|
62
62
|
import { openVault, defaultVaultPath } from "./store.js";
|
|
63
63
|
import { totp, parseTotpSecret, hotp, base32Decode } from "./totp.js";
|
|
64
64
|
import { generatePassword, generatePassphrase } from "./password.js";
|
|
65
|
-
import { checkPassword } from "./breach.js";
|
|
65
|
+
import { checkPassword, WEAK_PASSWORDS } from "./breach.js";
|
|
66
|
+
import { estimateStrength as estimateStrengthPure } from "./password-strength.js";
|
|
66
67
|
import { readChromeLogins, defaultChromeLoginData, defaultChromeLocalState } from "./chrome.js";
|
|
67
68
|
import { readKeychainPasswords, listKeychainEntries } from "./keychain.js";
|
|
68
69
|
import { analyzeVault, analyzeEntry } from "./watchtower.js";
|
|
@@ -6540,6 +6541,14 @@ function entryPasswordStrength(entry) {
|
|
|
6540
6541
|
const secret = passwordLikeSecret(entry);
|
|
6541
6542
|
return secret === undefined ? undefined : estimateStrength(secret).score;
|
|
6542
6543
|
}
|
|
6544
|
+
/** Strength with the bundled common-password list wired in, so a known-common
|
|
6545
|
+
* password cannot score well on length alone. */
|
|
6546
|
+
function estimateStrength(password) {
|
|
6547
|
+
const { score, verdict, feedback, bits } = estimateStrengthPure(password, WEAK_PASSWORDS);
|
|
6548
|
+
// The tools and the UI meter take one line of prose; the array stays available
|
|
6549
|
+
// to tests and to anything that wants the individual reasons.
|
|
6550
|
+
return { score, verdict, feedback: feedback.join('; '), bits };
|
|
6551
|
+
}
|
|
6543
6552
|
function toSummary(entry) {
|
|
6544
6553
|
const strength = entryPasswordStrength(entry);
|
|
6545
6554
|
return {
|
|
@@ -7179,58 +7188,8 @@ function pickDefinedFromRecord(record) {
|
|
|
7179
7188
|
}
|
|
7180
7189
|
/** Zero-dependency password strength estimator: score 0–100 from length,
|
|
7181
7190
|
* character-class coverage, and penalties for common weak patterns. */
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
const length = password.length;
|
|
7185
|
-
// Length is the dominant factor.
|
|
7186
|
-
score += Math.min(45, length * 3);
|
|
7187
|
-
const classes = [
|
|
7188
|
-
/[a-z]/.test(password),
|
|
7189
|
-
/[A-Z]/.test(password),
|
|
7190
|
-
/[0-9]/.test(password),
|
|
7191
|
-
/[^A-Za-z0-9]/.test(password),
|
|
7192
|
-
].filter(Boolean).length;
|
|
7193
|
-
score += classes * 8;
|
|
7194
|
-
// Diversity bonus for longer unique characters.
|
|
7195
|
-
const unique = new Set(password).size;
|
|
7196
|
-
score += Math.min(15, unique);
|
|
7197
|
-
// Penalties for common weak patterns.
|
|
7198
|
-
let feedback = [];
|
|
7199
|
-
if (length < 8)
|
|
7200
|
-
feedback.push('too short (aim ≥ 12)');
|
|
7201
|
-
if (/^(password|123456|qwerty|letmein|admin|welcome|abc123)$/i.test(password)) {
|
|
7202
|
-
score -= 40;
|
|
7203
|
-
feedback.push('common password');
|
|
7204
|
-
}
|
|
7205
|
-
if (/(.)\1{2,}/.test(password)) {
|
|
7206
|
-
score -= 8;
|
|
7207
|
-
feedback.push('repeated characters');
|
|
7208
|
-
}
|
|
7209
|
-
if (/^\d+$/.test(password)) {
|
|
7210
|
-
score -= 15;
|
|
7211
|
-
feedback.push('digits only');
|
|
7212
|
-
}
|
|
7213
|
-
if (/^[a-z]+$/i.test(password)) {
|
|
7214
|
-
score -= 10;
|
|
7215
|
-
feedback.push('letters only');
|
|
7216
|
-
}
|
|
7217
|
-
if (password.length > 0 && new Set(password).size <= Math.max(3, Math.floor(length / 2)))
|
|
7218
|
-
feedback.push('low diversity');
|
|
7219
|
-
score = Math.max(0, Math.min(100, Math.round(score)));
|
|
7220
|
-
const verdict = score >= 80 ? 'very strong' : score >= 60 ? 'strong' : score >= 40 ? 'fair' : 'weak';
|
|
7221
|
-
// Entropy estimate in bits: log2(pool size) per character.
|
|
7222
|
-
let pool = 0;
|
|
7223
|
-
if (/[a-z]/.test(password))
|
|
7224
|
-
pool += 26;
|
|
7225
|
-
if (/[A-Z]/.test(password))
|
|
7226
|
-
pool += 26;
|
|
7227
|
-
if (/[0-9]/.test(password))
|
|
7228
|
-
pool += 10;
|
|
7229
|
-
if (/[^A-Za-z0-9]/.test(password))
|
|
7230
|
-
pool += 33;
|
|
7231
|
-
const bits = password.length > 0 && pool > 0 ? Math.round(password.length * Math.log2(pool)) : 0;
|
|
7232
|
-
return { score, verdict, feedback: feedback.length > 0 ? feedback.join('; ') : 'no obvious weaknesses', bits };
|
|
7233
|
-
}
|
|
7191
|
+
// Strength lives in its own module so the CLI, the tools and the UI meter all
|
|
7192
|
+
// share one estimator (see src/password-strength.ts).
|
|
7234
7193
|
/** Quote a value for safe use in .env / shell export (single-quote, escaping
|
|
7235
7194
|
* embedded single quotes the POSIX way). */
|
|
7236
7195
|
/** Generate a random username from adjective/noun syllables + digits. */
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password strength, dependency-free.
|
|
3
|
+
*
|
|
4
|
+
* Two layers:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Composition** — length, character classes and diversity, the cheap
|
|
7
|
+
* signals every meter uses.
|
|
8
|
+
* 2. **Patterns** — what a cracker actually exploits: a known-common password
|
|
9
|
+
* (including l33t and trailing-digit variants), a keyboard walk, a sequence,
|
|
10
|
+
* a date, a repeated block, or "one word plus digits" (`Sunflower22`).
|
|
11
|
+
*
|
|
12
|
+
* A pattern does not just shave points: it **caps** the score, because a
|
|
13
|
+
* dictionary word with a capital and a couple of digits is not 67/100 material
|
|
14
|
+
* no matter how long it is. Composition alone rated exactly that kind of
|
|
15
|
+
* password "strong", which is the bug this module exists to fix.
|
|
16
|
+
*
|
|
17
|
+
* It is deliberately not zxcvbn: no bundled dictionary, no frequency ranking.
|
|
18
|
+
* The one wordlist it consults is the offline common-password set from
|
|
19
|
+
* `breach.ts`, so the strength meter and the breach report agree on what
|
|
20
|
+
* "known-common" means.
|
|
21
|
+
*/
|
|
22
|
+
import { WEAK_PASSWORDS } from "./breach.js";
|
|
23
|
+
/** Substitute the usual l33t characters so `P@ssw0rd` matches `password`. */
|
|
24
|
+
function deLeet(password) {
|
|
25
|
+
return password
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[4@]/g, 'a')
|
|
28
|
+
.replace(/[3€]/g, 'e')
|
|
29
|
+
.replace(/[1!|]/g, 'i')
|
|
30
|
+
.replace(/0/g, 'o')
|
|
31
|
+
.replace(/[5$]/g, 's')
|
|
32
|
+
.replace(/7\+/g, 't');
|
|
33
|
+
}
|
|
34
|
+
/** The password with digits/symbols trimmed off both ends (for dictionary hits). */
|
|
35
|
+
function coreLetters(password) {
|
|
36
|
+
return password.toLowerCase().replace(/^[^a-z]+|[^a-z]+$/g, '');
|
|
37
|
+
}
|
|
38
|
+
const KEYBOARD_RUNS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm', '1234567890', 'qazwsx', '1qaz2wsx'];
|
|
39
|
+
const SEQUENCES = ['abcdefghijklmnopqrstuvwxyz', '01234567890'];
|
|
40
|
+
/** True when the text contains a straight run of ≥4 characters from `source`. */
|
|
41
|
+
function hasRun(text, source) {
|
|
42
|
+
for (let i = 0; i + 4 <= text.length; i++) {
|
|
43
|
+
const chunk = text.slice(i, i + 4);
|
|
44
|
+
if (source.includes(chunk))
|
|
45
|
+
return true;
|
|
46
|
+
if (source.includes([...chunk].reverse().join('')))
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/** True for a repeated block: `abcabcabc`, `121212`. */
|
|
52
|
+
function isRepeatedBlock(password) {
|
|
53
|
+
for (let size = 1; size <= Math.floor(password.length / 3); size++) {
|
|
54
|
+
const block = password.slice(0, size);
|
|
55
|
+
if (block.repeat(Math.ceil(password.length / size)).slice(0, password.length) === password)
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* True for a plausible date or year. Deliberately strict about separators: a
|
|
62
|
+
* bare four-digit run is a PIN or a sequence (`1111`), not a date, and calling it
|
|
63
|
+
* one used to add noise to the feedback.
|
|
64
|
+
*/
|
|
65
|
+
function hasDate(text) {
|
|
66
|
+
if (/(19|20)\d{2}/.test(text))
|
|
67
|
+
return true;
|
|
68
|
+
return /\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4}/.test(text);
|
|
69
|
+
}
|
|
70
|
+
/** Score one password; `common` defaults to the bundled offline list. */
|
|
71
|
+
export function estimateStrength(password, common = WEAK_PASSWORDS) {
|
|
72
|
+
const feedback = [];
|
|
73
|
+
const patterns = [];
|
|
74
|
+
const lower = password.toLowerCase();
|
|
75
|
+
const deLeeted = deLeet(password);
|
|
76
|
+
const letters = coreLetters(password);
|
|
77
|
+
// ---- layer 1: composition ------------------------------------------------
|
|
78
|
+
let score = Math.min(45, password.length * 3);
|
|
79
|
+
const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter(re => re.test(password)).length;
|
|
80
|
+
score += classes * 8;
|
|
81
|
+
score += Math.min(15, new Set(password).size);
|
|
82
|
+
if (password.length < 8)
|
|
83
|
+
feedback.push('too short (aim ≥ 12)');
|
|
84
|
+
if (/^\d+$/.test(password)) {
|
|
85
|
+
score -= 15;
|
|
86
|
+
feedback.push('digits only');
|
|
87
|
+
}
|
|
88
|
+
else if (/^[a-z]+$/i.test(password)) {
|
|
89
|
+
score -= 10;
|
|
90
|
+
feedback.push('letters only');
|
|
91
|
+
}
|
|
92
|
+
if (/(.)\1{2,}/.test(password)) {
|
|
93
|
+
score -= 8;
|
|
94
|
+
patterns.push('repeats');
|
|
95
|
+
feedback.push('repeated characters');
|
|
96
|
+
}
|
|
97
|
+
// ---- layer 2: patterns, each of which caps the score ---------------------
|
|
98
|
+
//
|
|
99
|
+
// The caps are length-aware for the *soft* patterns. A year inside a 30
|
|
100
|
+
// character passphrase (`CorrectHorseBatteryStaple!2024`) is not what makes it
|
|
101
|
+
// guessable, but the same year in an 8 character password is. A known-common
|
|
102
|
+
// password and a repeated block stay capped no matter how long they are: those
|
|
103
|
+
// are weak at any length.
|
|
104
|
+
const long = password.length >= 20;
|
|
105
|
+
let cap = 100;
|
|
106
|
+
const isCommon = (value) => common.has(value) || common.has(deLeet(value));
|
|
107
|
+
if (isCommon(lower) || isCommon(deLeeted)) {
|
|
108
|
+
cap = Math.min(cap, 5);
|
|
109
|
+
patterns.push('common');
|
|
110
|
+
feedback.push('found in common-password lists');
|
|
111
|
+
}
|
|
112
|
+
else if (letters.length >= 4 && isCommon(letters)) {
|
|
113
|
+
// a known word plus a suffix/prefix: `sunshine2024`, `Passw0rd!`
|
|
114
|
+
cap = Math.min(cap, long ? 50 : 25);
|
|
115
|
+
patterns.push('common-word');
|
|
116
|
+
feedback.push('a common password with numbers or symbols around it');
|
|
117
|
+
}
|
|
118
|
+
else if (/^[a-z]{5,}\d{1,4}!?$/i.test(password) || /^\d{1,4}[a-z]{5,}!?$/i.test(password)) {
|
|
119
|
+
// one dictionary-looking word plus a short number: `Sunflower22`
|
|
120
|
+
cap = Math.min(cap, long ? 65 : 45);
|
|
121
|
+
patterns.push('word+digits');
|
|
122
|
+
feedback.push('looks like a word plus digits — a dictionary attack covers it');
|
|
123
|
+
}
|
|
124
|
+
if (KEYBOARD_RUNS.some(run => hasRun(lower, run))) {
|
|
125
|
+
cap = Math.min(cap, long ? 50 : 30);
|
|
126
|
+
patterns.push('keyboard');
|
|
127
|
+
feedback.push('keyboard pattern (qwerty / asdf / 1234)');
|
|
128
|
+
}
|
|
129
|
+
if (SEQUENCES.some(seq => hasRun(lower, seq))) {
|
|
130
|
+
cap = Math.min(cap, long ? 50 : 35);
|
|
131
|
+
patterns.push('sequence');
|
|
132
|
+
feedback.push('sequential characters (abcd / 4321)');
|
|
133
|
+
}
|
|
134
|
+
if (hasDate(password)) {
|
|
135
|
+
cap = Math.min(cap, long ? 70 : 45);
|
|
136
|
+
patterns.push('date');
|
|
137
|
+
feedback.push('contains a date or year');
|
|
138
|
+
}
|
|
139
|
+
if (isRepeatedBlock(password)) {
|
|
140
|
+
cap = Math.min(cap, 30);
|
|
141
|
+
patterns.push('repeat-block');
|
|
142
|
+
feedback.push('a short block repeated over and over');
|
|
143
|
+
}
|
|
144
|
+
if (new Set(password).size <= Math.max(3, Math.floor(password.length / 2))) {
|
|
145
|
+
patterns.push('low-diversity');
|
|
146
|
+
feedback.push('low character diversity');
|
|
147
|
+
}
|
|
148
|
+
score = Math.max(0, Math.min(100, Math.round(Math.min(score, cap))));
|
|
149
|
+
const verdict = score >= 80 ? 'very strong' : score >= 60 ? 'strong' : score >= 40 ? 'fair' : 'weak';
|
|
150
|
+
// Naive entropy: pool size per character. Reported next to the score, not
|
|
151
|
+
// used to derive it — a 20-character `qwertyqwertyqwertyq` has plenty of bits
|
|
152
|
+
// and is still terrible.
|
|
153
|
+
let pool = 0;
|
|
154
|
+
if (/[a-z]/.test(password))
|
|
155
|
+
pool += 26;
|
|
156
|
+
if (/[A-Z]/.test(password))
|
|
157
|
+
pool += 26;
|
|
158
|
+
if (/[0-9]/.test(password))
|
|
159
|
+
pool += 10;
|
|
160
|
+
if (/[^A-Za-z0-9]/.test(password))
|
|
161
|
+
pool += 33;
|
|
162
|
+
const bits = password.length > 0 && pool > 0 ? Math.round(password.length * Math.log2(pool)) : 0;
|
|
163
|
+
return {
|
|
164
|
+
score,
|
|
165
|
+
verdict,
|
|
166
|
+
feedback: feedback.length > 0 ? feedback : ['no obvious weaknesses'],
|
|
167
|
+
bits,
|
|
168
|
+
patterns,
|
|
169
|
+
};
|
|
170
|
+
}
|