CH1

为什么 BLE 聊天用不了 TLS

Step 1 / 16
BLE p2p 没 server / 没 PKI / 没 CA — TLS 的信任锚直接缺位 Noise 是协议构建器,按需拼出 mutual auth + identity hiding + PFS + KCI 抗性 A Alice iPhone · BLE B Bob iPhone · BLE BLE GATT 通道(短距离 · 离线) CA · 证书颁发机构 Server 也要在线签发证书 不可达 TLS 的"先去 CA 验证证书"假设彻底报废 但仍要双向认证 + 前向保密 + 身份不被旁观者识破 Noise_XX_25519_ChaChaPoly_SHA256 Pattern_DH_Cipher_Hash · 协议名即是配方 mutual auth 双方都验证对方身份 identity hiding static key 在密文里传 PFS 每会话新 ephemeral key KCI 抗性 单边私钥泄漏不能仿冒 XX_25519_ChaChaPoly_SHA256 名字里这四件原语 — 全部由 CryptoKit 提供 读懂 Noise spec 的钥匙:每个 token 要么放公钥,要么做 DH X25519 DH Curve25519 椭圆曲线 两人公开信道协商共享秘密 32B 私钥 → 32B 共享 ChaChaPoly AEAD 认证加密 (RFC 8439) 同时提供机密性 + 完整性 12B nonce + 16B tag SHA-256 单向哈希 累积 transcript 摘要 任意输入 → 32B 输出 HKDF 密钥派生 (RFC 5869) extract + expand 两步 底层 = HMAC-SHA256 Noise 协议名 = Pattern _ DH _ Cipher _ Hash XX _ 25519 _ ChaChaPoly _ SHA256 名字本身唯一确定握手流程,不需要协商 bitchat 全部由 Apple CryptoKit 拼出 — 没有第三方密码学依赖 Noise 记号系统 · token 字典 e → 发送方放 ephemeral 公钥(每会话新生成) s → 发送方放 static 公钥(长期身份;XX 中加密发送) ee → DH(发送方 ephemeral, 接收方 ephemeral) es → DH(发送方 ephemeral, 接收方 static) · initiator 发起 se → DH(发送方 static, 接收方 ephemeral) ss → DH(发送方 static, 接收方 static) · 仅 KK/IK 等模式用 当前发送方是 initiator(发起握手的一方) 当前发送方是 responder(应答的一方) 规则:小写=谁的公钥;两字母=做哪两把私公钥的 DH;箭头 = 当前 sender SymmetricState · 双账本:ck 累加 DH 输出,h 累加所有公开数据 两条规则:发/收公开字节调 MixHash,完成 DH 调 MixKey MixKey 之后内嵌 CipherState 就有 key,可做 AEAD 加密;nonce 永不复用 A Alice · initiator B Bob · responder Alice · SymmetricState ck = protocol_name (32B) h = protocol_name (32B) 初始时 ck = h = name Bob · SymmetricState ck = protocol_name (32B) h = protocol_name (32B) 双方初始一致 InitializeSymmetric(protocol_name) name ≤ 32B → h = name (右 pad 0); else h = SHA256(name); ck = h MixKey(dh_output) HKDF(ck, ikm) → 新 ck 同时给出 cipher key MixHash(data) h = SHA256(h ‖ data) 累积 transcript 规则:先 MixHash(公开字节),后 MixKey(DH 输出) 每个 token 都是这两个动作的组合 CipherState k = SymmetricKey (32B) n = UInt64 nonce, 起始 0 由 MixKey 派生,嵌在 SymmetricState 内 ChaChaPoly.seal 使用 (k, nonce=n, ad=h) 输出 = ciphertext + 16B tag n ← n + 1 ⚠️ 同一 k 上 nonce 复用 = 灾难(直接泄漏明文 XOR) CipherState 严格单调递增 nonce,永不回退 XX 第 1 条 → e · Alice 发临时公钥,明文(没 key 还没 MixKey) XX 第 2 条上半 ← e, ee · Bob 回 ephemeral,第一次 MixKey 出 cipher key XX 第 2 条下半 s, es · Bob 加密发 static 公钥;身份首次被保护 XX 第 3 条 → s, se · Alice 加密发 static;双向认证完成 A Alice · initiator B Bob · responder Alice ck / h ck: name… h: name… Bob ck / h ck: name… h: name… e_A ephemeral 公钥 → e_A_pub (明文) 空 payload(无 cipher key) + MixHash(e_A_pub) e_B ← e_B_pub ee DH(e_B_priv, e_A_pub) k₁ cipher key k₁ s_B 🔒 ← {s_B_pub}_k₁ (已加密) es → k₂ (MixKey es) → k₂ s_A 🔒 → {s_A_pub}_k₂ (已加密) se Split · 握手末尾把 ck 一分二,得到双方对调的 send/recv CipherState Transport phase · 握手轮次淡出,每条消息只是 sendCipher.encrypt,nonce 自增 A Alice B Bob — 三轮握手 (淡出) — ck (终态) 已累积 ee + es + se HKDF(ck, "") → Split c₁ initiator → responder SymmetricKey(32B), n=0 c₂ responder → initiator SymmetricKey(32B), n=0 Alice: send=c₁, recv=c₂ Bob: send=c₂, recv=c₁ handshake hash h 同时导出,可做 channel binding Alice.sendCipher k = c₁.k n = 0, 1, 2 ... 每加密一条 n++ Bob.recvCipher k = c₁.k n = 0, 1, 2 ... sliding-window 防 replay ct₀ (n=0) ct₁ (n=1) ct₂ (n=2) 握手包和密文包共享同一条 GATT characteristic,靠包类型字节区分 KCI 抗性 + PFS · static 私钥泄漏不能解过去,不能仿冒未来 peer ID 会变,fingerprint = SHA256(static_pub) 不变 — bitchat 跨 ID 复用 session 📡 BLE GATT Service serviceUUID = F47B…4B5C · characteristicUUID = A1B2…4C5D announce peerID 广播 type = 0x01 "我在这" handshake-1 → e_A_pub 明文 32B handshake-2 ← e_B, ee, s, es 含已加密 s_B handshake-3 → s, se 含已加密 s_A encrypted msg transport · sendCipher type = 0x10 didDiscover → didConnect → 收 announce → initiateNoiseHandshake → 三轮 GATT write/notify → established NoiseSessionManager · [peerID → NoiseSession] 未建立时排队握手;established 后所有 user payload 走 transport 加密 假设场景:攻击者今天偷到了 Alice 的 static 私钥 s_A A Alice ⚠️ s_A leaked 🛡️ 过去录的密文 still safe PFS:每会话 ephemeral key e e 早已 zeroize,无法重算 ck 🛡️ 未来仿冒 still safe KCI 抗性:XX 既有 es 又有 se 仅有 s_A 算不出 es (差 s_B) 和 TLS 1.3 PFS 类比 · TLS 仅靠 ephemeral 给 PFS;Noise XX 多一层 KCI 抗性 关键:握手期 ephemeral 用完立即 clearSensitiveData(),进 Split 后清零 📡 BLE channel F47B…4B5C A1B2…4C5D A fingerprint = SHA256(s_A_pub)[..8] 3f7a c9d2 … B fingerprint = SHA256(s_B_pub)[..8] b1e4 5af0 … 时间轴 · BLE 重连后 peerID 变了,但 fingerprint 不变 peerID = X9k2 第 1 次连 断连 peerID = M7p4 第 2 次连 (新 peerID) fingerprint 3f7a c9d2 … 持续不变 → bitchat 跨 ID 复用 NoiseSession rekey 策略:BRING_THE_NOISE.md 写 1h 或 10k 消息 实现细节待确认 — 源码未见显式 rekey() 方法,可能由 NoiseSessionManager 重建 session 实现