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 实现
◀ 上一步
下一步 ▶
↻ 重头来
键盘 ← / → 也能切;URL 自动保存当前步