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 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 分,★★★ = 满分**,中间按半星分档。半星不是 Unicode 字符(大多数字体没有),而是把同样三个 `★` 复制一层、按百分比裁剪彩色层实现的:
348
+ 列表里每个条目在标题后有一个**三星**强度指示 —— **☆☆☆ = 0 分,★★★ = 满分**,中间按半星分档:
349
+
350
+ ![强度指示的七种状态](docs/strength-stars.svg)
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. A half star is the coloured copy of
366
- the same three glyphs clipped to a percentage (Unicode has no half-star glyph
367
- most fonts can render):
395
+ ★★★ = full marks** — in six half-star steps:
396
+
397
+ ![strength indicator: the seven states](docs/strength-stars.svg)
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 | shows | band |
403
+ | score | filled stars | band |
370
404
  |---|---|---|
371
- | 0–8 | ☆☆☆ | weak |
372
- | 9–24 | ½☆☆ | weak |
373
- | 25–41 | ★☆☆ | weak |
374
- | 42–58 | ★½☆ | fair |
375
- | 59–74 | ★★☆ | fair |
376
- | 75–91 | ★★½ | strong |
377
- | 92–100 | ★★★ | strong |
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
- const WEAK_PASSWORDS = new Set([
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: { 'User-Agent': 'dsh-vault' },
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
- "lenChips": "t_j5va_lenChips",
514
- "totpRing": "t_j5va_totpRing",
515
- "actionGroupLabel": "t_j5va_actionGroupLabel",
516
- "detailActions": "t_j5va_detailActions",
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
- "rowDue": "t_j5va_rowDue",
520
- "dueBadge": "t_j5va_dueBadge",
521
- "rowCheck": "t_j5va_rowCheck",
522
- "editorBody": "t_j5va_editorBody",
523
- "auditRow": "t_j5va_auditRow",
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
- "customValueInput": "t_j5va_customValueInput",
528
- "toolbarMain": "t_j5va_toolbarMain",
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
- "favToggle": "t_j5va_favToggle",
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
- "exportChip": "t_j5va_exportChip",
535
- "row": "t_j5va_row",
568
+ "gridRow": "t_j5va_gridRow",
569
+ "policyBar": "t_j5va_policyBar",
536
570
  "addButton": "t_j5va_addButton",
537
- "lockedBanner": "t_j5va_lockedBanner",
538
- "detailRemain": "t_j5va_detailRemain",
539
- "strengthFair": "t_j5va_strengthFair",
540
- "searchBox": "t_j5va_searchBox",
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
- "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",
576
+ "recentClear": "t_j5va_recentClear",
561
577
  "kindChipActive": "t_j5va_kindChipActive",
562
- "listCompact": "t_j5va_listCompact",
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
- "pwHistPwd": "t_j5va_pwHistPwd",
572
- "gridActions": "t_j5va_gridActions",
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",
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
- "inlineField": "t_j5va_inlineField",
585
- "bulkItem": "t_j5va_bulkItem",
586
- "saveButton": "t_j5va_saveButton",
587
- "srOnly": "t_j5va_srOnly",
588
- "secretField": "t_j5va_secretField",
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
- "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",
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
- "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",
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
- "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
- "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
- "gridInput": "t_j5va_gridInput",
678
- "moreMenu": "t_j5va_moreMenu",
612
+ "uriCode": "t_j5va_uriCode",
679
613
  "section": "t_j5va_section",
680
- "pwWeak": "t_j5va_pwWeak",
681
- "kindChip": "t_j5va_kindChip",
682
- "vaultHeader": "t_j5va_vaultHeader",
683
- "strengthFill": "t_j5va_strengthFill",
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
- "pwHistTime": "t_j5va_pwHistTime",
686
- "reportJump": "t_j5va_reportJump",
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
- "emojiPicker": "t_j5va_emojiPicker",
698
- "tagActions": "t_j5va_tagActions",
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
- "histAdd": "t_j5va_histAdd",
701
- "status": "t_j5va_status",
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
- "lockedTag": "t_j5va_lockedTag"
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
- function estimateStrength(password) {
7183
- let score = 0;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vault",
3
- "version": "1.10.71",
3
+ "version": "1.10.73",
4
4
  "bin": {
5
5
  "dsh-vault": "./lib/cli.js"
6
6
  },