فهرست منبع

test(乐收赢): 密钥体检补一把宽量具 —— 扫全部 git 跟踪文件,不只 logs/

已有的 _lsy-key-scan.py 扫的是 **logs/ 下的 _lsy\* 产物**。那是「我准备提交什么」的量具,
回答不了另一个问题:**这个值在仓库里到底出现在哪。** 射程不同 ⇒ 加一把宽的。

_lsy-repo-keyscan.py 扫 git ls-files 的**全部 1233 个**跟踪文件的工作区版本,
外加 HEAD:application.yml(提交里那一份)。正对照:工作区 application.yml 必须命中 ≥1,
否则自判探针失效并非零退出。只打长度、sha256 前 16 位指纹、文件名、命中次数、
以及命中行的**字段名**(该行里的密钥整段替换成 <KEY>)。

读数(logs/_lsy-repo-keyscan.txt):工作区命中 **8 个文件**、HEAD:application.yml 命中 1。
逐个定位(-ctx 那份)之后,8 处**全部**是同一个东西,没有一处是新泄漏:

  LeshouyingProperties.java:65  static final String DOC_TEST_KEY = "<KEY>";
  application.yml:175(工作区)/ :171(HEAD)  key: ${LESHOUYING_KEY:<KEY>}
  5 个测试文件                   各自用同一个值做验签向量(不共用就会有 5 个不同的假值)
  .claude/plan/2026-09-21-….md   设计稿里的 yml 片段与测试片段(4 处)

为什么这不是泄漏:**它是厂商自己公布的测试凭证**。同一串字符出现在上游公开文档页
196236523(就是授权联调那一页)的表格行「机构秘钥key | … | 测试密钥」里,
配套的是 test.gdyfsk.com / inst_no=900112 / mch_no=Y52211090004 —— 全是测试环境。
我们用的就是它,这正是联调能解锁的原因。上一批次已就此裁定过一次。

为什么仍然值得记一笔:
1. 它在 application.yml 里是**环境变量占位符的默认值**(`${LESHOUYING_KEY:<值>}`)。
   生产部署必须真的设 LESHOUYING_KEY,否则应用会带着这把**公开的测试密钥**跑起来 ——
   守卫是 validate():非 dev profile 下见到文档测试值就拒绝启动(见 LeshouyingProperties:59-62)。
   本探针不改变那个判断,只是把「默认值确实在库里」变成可复现的读数。
2. logs/ 下那 3 个被抓出来的文档抓取件(_lsy-doc-196236523.md 等)**不入库** ——
   入库等于把这个值再搬进 git 一份,而它已经被有意排除在 application.yml 之外
   (application.yml 是 MM,私钥明文,绝不 git add)。体检脚本对它们的判定仍是「有泄漏,不得提交」,
   这是**量具的正确行为**:它不知道也不该知道哪个命中是公开的,它只报命中。

另查(换个量具,因为文件名是弱量具):那 3 个文档抓取件**从未进入 git 历史** ——
`git log --all --oneline -- *_lsy-page-*` 与按文件名逐个查均 0 命中,
`git log --all -S"机构秘钥"`(该表格独有的标记串)也是 0 命中。
⇒ 没有历史泄漏,只有工作区。

两个量具都在扫射程内自我覆盖(文件名以 _lsy 开头)—— 它们自身不含密钥,扫到 0 才算正常。
alphaH 2 هفته پیش
والد
کامیت
220f4ebc34
4فایلهای تغییر یافته به همراه181 افزوده شده و 0 حذف شده
  1. 8 0
      logs/_lsy-key-scan.txt
  2. 35 0
      logs/_lsy-repo-keyscan-ctx.txt
  3. 124 0
      logs/_lsy-repo-keyscan.py
  4. 14 0
      logs/_lsy-repo-keyscan.txt

+ 8 - 0
logs/_lsy-key-scan.txt

@@ -0,0 +1,8 @@
+密钥长度 = 32(只打长度)
+正对照 application.yml 命中 = 1  OK
+扫描文件数 = 82
+含密钥的文件数 = 3
+  !! logs\_lsy-doc-196236523.md  命中 1 次
+  !! logs\_lsy-page-196236523.json  命中 1 次
+  !! logs\_lsy-page-196236523.u8.json  命中 1 次
+结论: **有泄漏,不得提交**

+ 35 - 0
logs/_lsy-repo-keyscan-ctx.txt

@@ -0,0 +1,35 @@
+密钥长度 = 32, sha256[:16] = 19e1aef4419c6416(只打长度与不可逆指纹)
+正对照 工作区 application.yml 命中 = 1  OK
+扫描 git 跟踪文件数 = 1233(其中读不到 2)
+工作区版本含密钥的文件数 = 8
+  !! .claude/plan/2026-09-21-leshouying-payment-channel.md  命中 4 次
+  !! java/src/main/java/com/payment/platform/module/payment/leshouying/config/LeshouyingProperties.java  命中 1 次
+  !! java/src/main/resources/application.yml  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/client/LeshouyingClientTest.java  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/client/LeshouyingSignerTest.java  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingNotifyControllerTest.java  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyNotifyServiceTest.java  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingNotifyServiceTest.java  命中 1 次
+--- HEAD:application.yml ---
+bytes = 6321, 命中 = 1
+
+=== 命中行定位(密钥已整段替换成 <KEY>)===
+--- .claude/plan/2026-09-21-leshouying-payment-channel.md  (465312 B)
+   #1 第 137 行: private static final String KEY = "<KEY>";
+   #2 第 2493 行: key: ${LESHOUYING_KEY:<KEY>}
+   #3 第 2545 行: props.setKey("<KEY>");
+   #4 第 5588 行: private static final String KEY = "<KEY>";
+--- java/src/main/java/com/payment/platform/module/payment/leshouying/config/LeshouyingProperties.java  (17079 B)
+   #1 第 65 行: static final String DOC_TEST_KEY = "<KEY>";
+--- java/src/test/java/com/payment/platform/module/payment/leshouying/client/LeshouyingClientTest.java  (6980 B)
+   #1 第 37 行: props.setKey("<KEY>");
+--- java/src/test/java/com/payment/platform/module/payment/leshouying/client/LeshouyingSignerTest.java  (15123 B)
+   #1 第 13 行: private static final String KEY = "<KEY>";
+--- java/src/test/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingNotifyControllerTest.java  (11015 B)
+   #1 第 61 行: private static final String KEY = "<KEY>";
+--- java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyNotifyServiceTest.java  (63032 B)
+   #1 第 49 行: private static final String KEY = "<KEY>";
+--- java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingNotifyServiceTest.java  (25324 B)
+   #1 第 58 行: private static final String KEY = "<KEY>";
+--- 工作区 application.yml  第 175 行: key: ${LESHOUYING_KEY:<KEY>}
+--- HEAD:application.yml  第 171 行: key: ${LESHOUYING_KEY:<KEY>}

+ 124 - 0
logs/_lsy-repo-keyscan.py

@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+"""全库体检:机构密钥有没有出现在**任何 git 跟踪文件**里(不限 logs/)。
+
+为什么要有它:_lsy-key-scan.py 只扫 logs/ 下的 _lsy* 产物 —— 那是「我准备提交什么」的量具。
+但「仓库里有没有这个值」是另一个问题,射程不同:本探针扫 git ls-files 的**全部 1233 个**跟踪
+文件的工作区版本,外加 HEAD:application.yml(索引/提交里那一份)。
+两个量具都留着:前者窄而针对提交动作,后者宽而针对仓库状态。
+
+只打:密钥长度、密钥 sha256 前 16 位(不可逆指纹)、文件名、命中次数、**命中行的字段名**
+(该行里的密钥整段替换成 <KEY>)。绝不打印密钥本身或其任何子串。
+
+正对照:工作区的 application.yml 必须命中 ≥1 次,否则自判「探针失效」并以非零退出。
+
+用法:
+    python logs/_lsy-repo-keyscan.py        # 计数版(默认,打在 stdout)
+    python logs/_lsy-repo-keyscan.py --ctx  # 定位版:每个命中处的整行(密钥已替换)
+"""
+import io
+import os
+import re
+import subprocess
+import sys
+from hashlib import sha256
+
+YML = "java/src/main/resources/application.yml"
+VAR = re.compile(r"^\$\{([A-Za-z0-9_]+):(.*)\}$")
+
+
+def read_block(path, top):
+    lines = io.open(path, encoding="utf-8", errors="replace").read().split("\n")
+    start = None
+    for i, ln in enumerate(lines):
+        if re.match(r"^%s:\s*$" % re.escape(top), ln):
+            start = i
+            break
+    if start is None:
+        raise SystemExit("找不到顶层段 %s:" % top)
+    out = {}
+    for j in range(start + 1, len(lines)):
+        ln = lines[j]
+        if ln.strip() == "" or ln.lstrip().startswith("#"):
+            continue
+        if not ln.startswith((" ", "\t")):
+            break
+        m = re.match(r"^\s+([A-Za-z0-9_.\-]+):\s*(.*)$", ln)
+        if m:
+            out[m.group(1)] = re.split(r"\s+#", m.group(2), maxsplit=1)[0].strip().strip("'\"")
+    return out
+
+
+cfg = read_block(YML, "leshouying")
+raw = cfg.get("key", "")
+m = VAR.match(raw)
+KEY = (os.environ.get(m.group(1)) or m.group(2)) if m else raw
+needle = KEY.encode("utf-8")
+print("密钥长度 = %d, sha256[:16] = %s(只打长度与不可逆指纹)"
+      % (len(KEY), sha256(needle).hexdigest()[:16]))
+
+pos = io.open(YML, "rb").read().count(needle)
+print("正对照 工作区 application.yml 命中 = %d  %s"
+      % (pos, "OK" if pos >= 1 else "!! 探针失效,体检作废"))
+if pos < 1:
+    raise SystemExit(1)
+
+tracked = subprocess.run(["git", "ls-files", "-z"], capture_output=True).stdout.split(b"\0")
+tracked = [t.decode("utf-8", "replace") for t in tracked if t]
+
+bad, missing = [], 0
+for f in tracked:
+    try:
+        c = io.open(f, "rb").read().count(needle)
+    except Exception:
+        missing += 1
+        continue
+    if c:
+        bad.append((f, c))
+
+print("扫描 git 跟踪文件数 = %d(其中读不到 %d)" % (len(tracked), missing))
+print("工作区版本含密钥的文件数 = %d" % len(bad))
+for f, c in bad:
+    print("  !! %s  命中 %d 次" % (f, c))
+
+head = subprocess.run(["git", "show", "HEAD:java/src/main/resources/application.yml"],
+                      capture_output=True).stdout
+print("--- HEAD:application.yml ---")
+print("bytes = %d, 命中 = %d" % (len(head), head.count(needle)))
+
+if "--ctx" not in sys.argv:
+    raise SystemExit(0)
+
+# 定位版:把每个命中处的**整行**打出来(该行里的密钥替换成 <KEY>)
+print()
+print("=== 命中行定位(密钥已整段替换成 <KEY>)===")
+for f, _c in bad:
+    if f == YML:
+        continue          # 工作区那份单独打在下面
+    blob = io.open(f, "rb").read()
+    print("--- %s  (%d B)" % (f, len(blob)))
+    start, k = 0, 0
+    while True:
+        i = blob.find(needle, start)
+        if i < 0:
+            break
+        k += 1
+        ls = blob.rfind(b"\n", 0, i) + 1
+        le = blob.find(b"\n", i)
+        if le < 0:
+            le = len(blob)
+        line = blob[ls:le].decode("utf-8", "replace").replace(KEY, "<KEY>")
+        print("   #%d 第 %d 行: %s" % (k, blob.count(b"\n", 0, ls) + 1, line.strip()))
+        start = i + len(needle)
+
+for label, blob in (("工作区 application.yml", io.open(YML, "rb").read()),
+                    ("HEAD:application.yml", head)):
+    i = blob.find(needle)
+    if i < 0:
+        continue
+    ls = blob.rfind(b"\n", 0, i) + 1
+    le = blob.find(b"\n", i)
+    if le < 0:
+        le = len(blob)
+    print("--- %s  第 %d 行: %s"
+          % (label, blob.count(b"\n", 0, ls) + 1,
+             blob[ls:le].decode("utf-8", "replace").replace(KEY, "<KEY>").strip()))

+ 14 - 0
logs/_lsy-repo-keyscan.txt

@@ -0,0 +1,14 @@
+密钥长度 = 32, sha256[:16] = 19e1aef4419c6416(只打长度与不可逆指纹)
+正对照 工作区 application.yml 命中 = 1  OK
+扫描 git 跟踪文件数 = 1233(其中读不到 2)
+工作区版本含密钥的文件数 = 8
+  !! .claude/plan/2026-09-21-leshouying-payment-channel.md  命中 4 次
+  !! java/src/main/java/com/payment/platform/module/payment/leshouying/config/LeshouyingProperties.java  命中 1 次
+  !! java/src/main/resources/application.yml  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/client/LeshouyingClientTest.java  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/client/LeshouyingSignerTest.java  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingNotifyControllerTest.java  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyNotifyServiceTest.java  命中 1 次
+  !! java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingNotifyServiceTest.java  命中 1 次
+--- HEAD:application.yml ---
+bytes = 6321, 命中 = 1