dsh-vault 1.10.71 → 1.10.72
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 +19 -0
- package/README.md +30 -0
- package/lib/breach.js +10 -3
- package/lib/client.js +167 -167
- package/lib/index.js +12 -53
- package/lib/password-strength.js +170 -0
- package/package.json +1 -1
package/README-zh.md
CHANGED
|
@@ -324,6 +324,25 @@ 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
348
|
列表里每个条目在标题后有一个**三星**强度指示 —— **☆☆☆ = 0 分,★★★ = 满分**,中间按半星分档。半星不是 Unicode 字符(大多数字体没有),而是把同样三个 `★` 复制一层、按百分比裁剪彩色层实现的:
|
package/README.md
CHANGED
|
@@ -359,6 +359,36 @@ 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,
|
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
|
-
"
|
|
517
|
-
"
|
|
518
|
-
"
|
|
519
|
-
"
|
|
520
|
-
"
|
|
521
|
-
"
|
|
522
|
-
"
|
|
523
|
-
"
|
|
513
|
+
"brandGlyph": "t_j5va_brandGlyph",
|
|
514
|
+
"intro": "t_j5va_intro",
|
|
515
|
+
"reportTitle": "t_j5va_reportTitle",
|
|
516
|
+
"colorSwatch": "t_j5va_colorSwatch",
|
|
517
|
+
"recentLabel": "t_j5va_recentLabel",
|
|
518
|
+
"pinStar": "t_j5va_pinStar",
|
|
519
|
+
"detailItem": "t_j5va_detailItem",
|
|
520
|
+
"tagCount": "t_j5va_tagCount",
|
|
521
|
+
"pinOn": "t_j5va_pinOn",
|
|
522
|
+
"toolbarFilters": "t_j5va_toolbarFilters",
|
|
523
|
+
"title": "t_j5va_title",
|
|
524
|
+
"toolbarDivider": "t_j5va_toolbarDivider",
|
|
525
|
+
"importName": "t_j5va_importName",
|
|
526
|
+
"searchBox": "t_j5va_searchBox",
|
|
527
|
+
"recentClear": "t_j5va_recentClear",
|
|
528
|
+
"strengthFair": "t_j5va_strengthFair",
|
|
524
529
|
"totpCopy": "t_j5va_totpCopy",
|
|
525
|
-
"
|
|
526
|
-
"
|
|
527
|
-
"customValueInput": "t_j5va_customValueInput",
|
|
528
|
-
"toolbarMain": "t_j5va_toolbarMain",
|
|
530
|
+
"importActions": "t_j5va_importActions",
|
|
531
|
+
"auditChipActive": "t_j5va_auditChipActive",
|
|
529
532
|
"gridDetail": "t_j5va_gridDetail",
|
|
530
|
-
"
|
|
531
|
-
"
|
|
532
|
-
"
|
|
533
|
-
"
|
|
534
|
-
"
|
|
535
|
-
"
|
|
536
|
-
"
|
|
537
|
-
"
|
|
538
|
-
"
|
|
539
|
-
"
|
|
540
|
-
"
|
|
533
|
+
"tabs": "t_j5va_tabs",
|
|
534
|
+
"highBadge": "t_j5va_highBadge",
|
|
535
|
+
"lockedTag": "t_j5va_lockedTag",
|
|
536
|
+
"customValueInput": "t_j5va_customValueInput",
|
|
537
|
+
"kindChip": "t_j5va_kindChip",
|
|
538
|
+
"emptyHint": "t_j5va_emptyHint",
|
|
539
|
+
"dupMerge": "t_j5va_dupMerge",
|
|
540
|
+
"importList": "t_j5va_importList",
|
|
541
|
+
"colorFieldWrap": "t_j5va_colorFieldWrap",
|
|
542
|
+
"badgeDanger": "t_j5va_badgeDanger",
|
|
543
|
+
"kindIcon": "t_j5va_kindIcon",
|
|
541
544
|
"pwBar": "t_j5va_pwBar",
|
|
542
|
-
"
|
|
543
|
-
"
|
|
544
|
-
"
|
|
545
|
-
"
|
|
546
|
-
"
|
|
545
|
+
"status": "t_j5va_status",
|
|
546
|
+
"actionGroups": "t_j5va_actionGroups",
|
|
547
|
+
"actionGroupLabel": "t_j5va_actionGroupLabel",
|
|
548
|
+
"genHist": "t_j5va_genHist",
|
|
549
|
+
"badgeOk": "t_j5va_badgeOk",
|
|
550
|
+
"gridActions": "t_j5va_gridActions",
|
|
551
|
+
"pinOff": "t_j5va_pinOff",
|
|
552
|
+
"dupNames": "t_j5va_dupNames",
|
|
553
|
+
"row": "t_j5va_row",
|
|
547
554
|
"riskDanger": "t_j5va_riskDanger",
|
|
548
|
-
"badgeBtn": "t_j5va_badgeBtn",
|
|
549
|
-
"histDanger": "t_j5va_histDanger",
|
|
550
|
-
"emptyBox": "t_j5va_emptyBox",
|
|
551
|
-
"tabPane": "t_j5va_tabPane",
|
|
552
|
-
"reportLineLink": "t_j5va_reportLineLink",
|
|
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",
|
|
561
|
-
"kindChipActive": "t_j5va_kindChipActive",
|
|
562
|
-
"listCompact": "t_j5va_listCompact",
|
|
563
|
-
"identity": "t_j5va_identity",
|
|
564
|
-
"actionPrimary": "t_j5va_actionPrimary",
|
|
565
555
|
"pwHistRow": "t_j5va_pwHistRow",
|
|
566
|
-
"
|
|
556
|
+
"healthSummary": "t_j5va_healthSummary",
|
|
557
|
+
"deleteButton": "t_j5va_deleteButton",
|
|
558
|
+
"tagManagerHead": "t_j5va_tagManagerHead",
|
|
559
|
+
"reportBox": "t_j5va_reportBox",
|
|
560
|
+
"pwWeak": "t_j5va_pwWeak",
|
|
561
|
+
"rowActions": "t_j5va_rowActions",
|
|
562
|
+
"moreWrap": "t_j5va_moreWrap",
|
|
563
|
+
"pwHistTime": "t_j5va_pwHistTime",
|
|
564
|
+
"gridInput": "t_j5va_gridInput",
|
|
565
|
+
"auditChips": "t_j5va_auditChips",
|
|
566
|
+
"totpRing": "t_j5va_totpRing",
|
|
567
|
+
"histAdd": "t_j5va_histAdd",
|
|
568
|
+
"listCompact": "t_j5va_listCompact",
|
|
569
|
+
"modeAsk": "t_j5va_modeAsk",
|
|
570
|
+
"scoreRing": "t_j5va_scoreRing",
|
|
571
|
+
"strengthWeak": "t_j5va_strengthWeak",
|
|
572
|
+
"colorPalette": "t_j5va_colorPalette",
|
|
573
|
+
"toolbar": "t_j5va_toolbar",
|
|
574
|
+
"tagManager": "t_j5va_tagManager",
|
|
575
|
+
"reportSub": "t_j5va_reportSub",
|
|
576
|
+
"modeAuto": "t_j5va_modeAuto",
|
|
577
|
+
"pwMeter": "t_j5va_pwMeter",
|
|
578
|
+
"pwStrong": "t_j5va_pwStrong",
|
|
579
|
+
"customFieldRows": "t_j5va_customFieldRows",
|
|
580
|
+
"customFieldRow": "t_j5va_customFieldRow",
|
|
581
|
+
"customKeyInput": "t_j5va_customKeyInput",
|
|
582
|
+
"favToggle": "t_j5va_favToggle",
|
|
583
|
+
"strengthStrong": "t_j5va_strengthStrong",
|
|
567
584
|
"editorActions": "t_j5va_editorActions",
|
|
568
|
-
"
|
|
569
|
-
"
|
|
570
|
-
"
|
|
571
|
-
"
|
|
572
|
-
"
|
|
573
|
-
"tabActive": "t_j5va_tabActive",
|
|
574
|
-
"inlineInput": "t_j5va_inlineInput",
|
|
575
|
-
"uriCode": "t_j5va_uriCode",
|
|
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",
|
|
582
|
-
"recentLabel": "t_j5va_recentLabel",
|
|
583
|
-
"badgeActive": "t_j5va_badgeActive",
|
|
584
|
-
"inlineField": "t_j5va_inlineField",
|
|
585
|
-
"bulkItem": "t_j5va_bulkItem",
|
|
586
|
-
"saveButton": "t_j5va_saveButton",
|
|
585
|
+
"reportJump": "t_j5va_reportJump",
|
|
586
|
+
"reportLineLink": "t_j5va_reportLineLink",
|
|
587
|
+
"rowSelected": "t_j5va_rowSelected",
|
|
588
|
+
"histLink": "t_j5va_histLink",
|
|
589
|
+
"favActive": "t_j5va_favActive",
|
|
587
590
|
"srOnly": "t_j5va_srOnly",
|
|
588
|
-
"
|
|
589
|
-
"
|
|
590
|
-
"
|
|
591
|
+
"uriCode": "t_j5va_uriCode",
|
|
592
|
+
"badgeLink": "t_j5va_badgeLink",
|
|
593
|
+
"importDesc": "t_j5va_importDesc",
|
|
591
594
|
"selectActive": "t_j5va_selectActive",
|
|
592
|
-
"
|
|
595
|
+
"emojiBtn": "t_j5va_emojiBtn",
|
|
596
|
+
"detailBox": "t_j5va_detailBox",
|
|
597
|
+
"detailDeleteBtn": "t_j5va_detailDeleteBtn",
|
|
593
598
|
"badgeWarn": "t_j5va_badgeWarn",
|
|
594
|
-
"
|
|
595
|
-
"
|
|
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",
|
|
599
|
+
"sortButton": "t_j5va_sortButton",
|
|
600
|
+
"recentChip": "t_j5va_recentChip",
|
|
601
601
|
"revealButton": "t_j5va_revealButton",
|
|
602
|
-
"
|
|
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",
|
|
608
|
-
"toolbarFilters": "t_j5va_toolbarFilters",
|
|
609
|
-
"clearButton": "t_j5va_clearButton",
|
|
610
|
-
"rowActions": "t_j5va_rowActions",
|
|
611
|
-
"rowMain": "t_j5va_rowMain",
|
|
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",
|
|
602
|
+
"lockedBanner": "t_j5va_lockedBanner",
|
|
619
603
|
"bulkBar": "t_j5va_bulkBar",
|
|
620
|
-
"
|
|
621
|
-
"
|
|
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
|
-
"title": "t_j5va_title",
|
|
635
|
-
"tagManagerHead": "t_j5va_tagManagerHead",
|
|
636
|
-
"importRow": "t_j5va_importRow",
|
|
637
|
-
"vaultTarget": "t_j5va_vaultTarget",
|
|
638
|
-
"emptyHint": "t_j5va_emptyHint",
|
|
639
|
-
"importName": "t_j5va_importName",
|
|
640
|
-
"badgeDanger": "t_j5va_badgeDanger",
|
|
641
|
-
"detailCode": "t_j5va_detailCode",
|
|
604
|
+
"rowCheck": "t_j5va_rowCheck",
|
|
605
|
+
"customEq": "t_j5va_customEq",
|
|
642
606
|
"copied": "t_j5va_copied",
|
|
643
|
-
"
|
|
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",
|
|
607
|
+
"badge": "t_j5va_badge",
|
|
649
608
|
"urlGlyph": "t_j5va_urlGlyph",
|
|
650
|
-
"auditIssues": "t_j5va_auditIssues",
|
|
651
|
-
"riskBanner": "t_j5va_riskBanner",
|
|
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
609
|
"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
610
|
"moreButton": "t_j5va_moreButton",
|
|
670
|
-
"
|
|
671
|
-
"
|
|
672
|
-
"
|
|
673
|
-
"
|
|
674
|
-
"
|
|
675
|
-
"
|
|
611
|
+
"emptyBox": "t_j5va_emptyBox",
|
|
612
|
+
"resultCount": "t_j5va_resultCount",
|
|
613
|
+
"dangerButton": "t_j5va_dangerButton",
|
|
614
|
+
"detailRemain": "t_j5va_detailRemain",
|
|
615
|
+
"pwHistPwd": "t_j5va_pwHistPwd",
|
|
616
|
+
"exportChip": "t_j5va_exportChip",
|
|
617
|
+
"vaultSwitch": "t_j5va_vaultSwitch",
|
|
618
|
+
"reportLine": "t_j5va_reportLine",
|
|
676
619
|
"gridName": "t_j5va_gridName",
|
|
677
|
-
"
|
|
620
|
+
"rowDue": "t_j5va_rowDue",
|
|
621
|
+
"tagRow": "t_j5va_tagRow",
|
|
622
|
+
"auditWhen": "t_j5va_auditWhen",
|
|
623
|
+
"vaultManageRow": "t_j5va_vaultManageRow",
|
|
624
|
+
"hit": "t_j5va_hit",
|
|
625
|
+
"tagName": "t_j5va_tagName",
|
|
626
|
+
"actionGroup": "t_j5va_actionGroup",
|
|
627
|
+
"brandGlyphSm": "t_j5va_brandGlyphSm",
|
|
628
|
+
"auditChip": "t_j5va_auditChip",
|
|
629
|
+
"badgeBtn": "t_j5va_badgeBtn",
|
|
630
|
+
"inlineInput": "t_j5va_inlineInput",
|
|
678
631
|
"moreMenu": "t_j5va_moreMenu",
|
|
679
|
-
"
|
|
680
|
-
"
|
|
681
|
-
"
|
|
632
|
+
"policyField": "t_j5va_policyField",
|
|
633
|
+
"identity": "t_j5va_identity",
|
|
634
|
+
"totp": "t_j5va_totp",
|
|
635
|
+
"policyBar": "t_j5va_policyBar",
|
|
636
|
+
"strengthStars": "t_j5va_strengthStars",
|
|
637
|
+
"tabPane": "t_j5va_tabPane",
|
|
638
|
+
"clearButton": "t_j5va_clearButton",
|
|
639
|
+
"attachAdd": "t_j5va_attachAdd",
|
|
640
|
+
"exportChips": "t_j5va_exportChips",
|
|
641
|
+
"emojiPicker": "t_j5va_emojiPicker",
|
|
642
|
+
"tab": "t_j5va_tab",
|
|
643
|
+
"recentSearch": "t_j5va_recentSearch",
|
|
644
|
+
"editorBody": "t_j5va_editorBody",
|
|
645
|
+
"error": "t_j5va_error",
|
|
646
|
+
"healthBar": "t_j5va_healthBar",
|
|
647
|
+
"riskWarn": "t_j5va_riskWarn",
|
|
648
|
+
"detailCode": "t_j5va_detailCode",
|
|
649
|
+
"tabActive": "t_j5va_tabActive",
|
|
682
650
|
"vaultHeader": "t_j5va_vaultHeader",
|
|
683
|
-
"
|
|
684
|
-
"
|
|
685
|
-
"
|
|
686
|
-
"
|
|
687
|
-
"
|
|
688
|
-
"vaultSwitch": "t_j5va_vaultSwitch",
|
|
689
|
-
"reportLine": "t_j5va_reportLine",
|
|
651
|
+
"strengthTrack": "t_j5va_strengthTrack",
|
|
652
|
+
"kindChipActive": "t_j5va_kindChipActive",
|
|
653
|
+
"importRow": "t_j5va_importRow",
|
|
654
|
+
"headerMessage": "t_j5va_headerMessage",
|
|
655
|
+
"genOptRow": "t_j5va_genOptRow",
|
|
690
656
|
"modeReadonly": "t_j5va_modeReadonly",
|
|
691
|
-
"
|
|
692
|
-
"
|
|
657
|
+
"section": "t_j5va_section",
|
|
658
|
+
"list": "t_j5va_list",
|
|
659
|
+
"empty": "t_j5va_empty",
|
|
660
|
+
"rowExpired": "t_j5va_rowExpired",
|
|
661
|
+
"attachBox": "t_j5va_attachBox",
|
|
662
|
+
"checkField": "t_j5va_checkField",
|
|
663
|
+
"toolbarMain": "t_j5va_toolbarMain",
|
|
664
|
+
"detailActions": "t_j5va_detailActions",
|
|
665
|
+
"vaultTarget": "t_j5va_vaultTarget",
|
|
693
666
|
"bulkCount": "t_j5va_bulkCount",
|
|
667
|
+
"genOpts": "t_j5va_genOpts",
|
|
694
668
|
"searchKbd": "t_j5va_searchKbd",
|
|
695
|
-
"modeAuto": "t_j5va_modeAuto",
|
|
696
|
-
"trashButton": "t_j5va_trashButton",
|
|
697
|
-
"emojiPicker": "t_j5va_emojiPicker",
|
|
698
669
|
"tagActions": "t_j5va_tagActions",
|
|
670
|
+
"copyAllBtn": "t_j5va_copyAllBtn",
|
|
671
|
+
"pwLabel": "t_j5va_pwLabel",
|
|
672
|
+
"addButton": "t_j5va_addButton",
|
|
673
|
+
"badgeActive": "t_j5va_badgeActive",
|
|
674
|
+
"lenChip": "t_j5va_lenChip",
|
|
675
|
+
"editor": "t_j5va_editor",
|
|
676
|
+
"actionPrimary": "t_j5va_actionPrimary",
|
|
677
|
+
"histUpdate": "t_j5va_histUpdate",
|
|
678
|
+
"kindFilter": "t_j5va_kindFilter",
|
|
679
|
+
"pwFair": "t_j5va_pwFair",
|
|
680
|
+
"rowMain": "t_j5va_rowMain",
|
|
681
|
+
"genHistItem": "t_j5va_genHistItem",
|
|
682
|
+
"gridRow": "t_j5va_gridRow",
|
|
683
|
+
"field": "t_j5va_field",
|
|
684
|
+
"auditIssues": "t_j5va_auditIssues",
|
|
685
|
+
"histDanger": "t_j5va_histDanger",
|
|
686
|
+
"lenChipActive": "t_j5va_lenChipActive",
|
|
687
|
+
"secretField": "t_j5va_secretField",
|
|
688
|
+
"dueBadge": "t_j5va_dueBadge",
|
|
689
|
+
"riskBanner": "t_j5va_riskBanner",
|
|
690
|
+
"dupGroup": "t_j5va_dupGroup",
|
|
691
|
+
"actionBtn": "t_j5va_actionBtn",
|
|
692
|
+
"lenChips": "t_j5va_lenChips",
|
|
693
|
+
"strengthFill": "t_j5va_strengthFill",
|
|
694
|
+
"bulkItem": "t_j5va_bulkItem",
|
|
695
|
+
"histNeutral": "t_j5va_histNeutral",
|
|
699
696
|
"detailTotp": "t_j5va_detailTotp",
|
|
700
|
-
"
|
|
701
|
-
"status": "t_j5va_status",
|
|
697
|
+
"iconFieldWrap": "t_j5va_iconFieldWrap",
|
|
702
698
|
"pwHistBox": "t_j5va_pwHistBox",
|
|
703
|
-
"
|
|
699
|
+
"inlineField": "t_j5va_inlineField",
|
|
700
|
+
"saveButton": "t_j5va_saveButton",
|
|
701
|
+
"backupButton": "t_j5va_backupButton",
|
|
702
|
+
"auditRow": "t_j5va_auditRow",
|
|
703
|
+
"trashButton": "t_j5va_trashButton"
|
|
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
|
+
}
|