第三十课:综合实战——构建一个安全的 Android 即时通讯 App

终于到了最后一章。说实话,写到这里我有点感慨。前面二十九章我们聊了证书链、TLS 握手、双向认证、证书锁定、ALPN、协议降级攻击…… 零零碎碎的知识点,今天我们要把它们全部串起来。

这一章,我会带你手搭一个即时通讯 App 的安全通信模块。不是 Demo,是真正能上线的架构。我在项目中踩过的坑,也会一并告诉你。

整体架构:安全通信的四层防线

先看整体设计。一个 IM App 的安全通信,不能只靠 TLS。我习惯把它拆成四层:

四层安全防线

  • 传输层:TLS 1.3 + 证书锁定 + ALPN
  • 会话层:端到端加密密钥协商(X3DH)
  • 消息层:每条消息独立加密(AES-256-GCM)
  • 应用层:防重放、防篡改、消息顺序校验

为什么要分四层?你想想看,如果只靠 TLS,服务端是可以看到明文消息的。对于 IM 来说,这显然不够。端到端加密必须自己做。

应用层:防重放、防篡改、消息顺序校验 每条消息携带序号 + HMAC,接收方校验 消息层:AES-256-GCM 每条消息独立加密 每个消息使用随机 Nonce,密钥来自会话层 会话层:X3DH 密钥协商 + 双棘轮算法 每次会话生成独立根密钥,前向安全 传输层:TLS 1.3 + 证书锁定 + ALPN OkHttp + 自定义 TrustManager,锁定服务端公钥

第一步:传输层——TLS 1.3 + 证书锁定

传输层是一切的基础。我个人习惯用 OkHttp 做网络层,配合自定义的 TrustManager 做证书锁定。注意,这里锁定的是公钥哈希,不是证书本身。为什么?因为证书会过期,公钥不会。

// 证书锁定——锁定服务端公钥哈希
class PinningTrustManager : X509ExtendedTrustManager() {
    private val expectedPin = "sha256/47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="

    override fun checkServerTrusted(chain: Array<X509Certificate>?, authType: String?, socket: SSLSocket?) {
        val cert = chain?.getOrNull(0) ?: throw SSLException("No certificate")
        val publicKey = cert.publicKey.encoded
        val digest = MessageDigest.getInstance("SHA-256").digest(publicKey)
        val pin = Base64.encodeToString(digest, Base64.NO_WRAP)
        if (pin != expectedPin) {
            throw SSLException("Certificate pin mismatch")
        }
    }
    // 其他方法省略...
}

我曾经踩过的坑:有一次线上事故,服务端换了硬件安全模块,公钥变了,但运维没通知客户端团队。结果所有用户连接失败。从那以后,我要求运维每次变更证书前,必须提前一周通知,并且客户端要保留旧 pin 做灰度过渡。

第二步:会话层——X3DH 密钥协商

传输层搞定后,接下来是端到端加密。我推荐 Signal 协议里的 X3DH(扩展的三方 Diffie-Hellman)。说白了,就是让两个用户在不安全的信道上,协商出一个只有他俩知道的共享密钥。

流程大致是这样:

  • 每个用户注册时,上传自己的身份公钥、签名预密钥、一批一次性预密钥
  • Alice 想给 Bob 发消息,先拉取 Bob 的密钥包
  • Alice 用自己的身份密钥 + Bob 的身份密钥 + Bob 的签名预密钥 + 一个一次性预密钥,做四次 DH 运算
  • 最终得到一个 32 字节的共享根密钥
// X3DH 密钥协商——简化示例
fun x3dhKeyExchange(
    aliceIdentity: KeyPair,
    bobIdentityKey: PublicKey,
    bobSignedPreKey: PublicKey,
    bobOneTimePreKey: PublicKey?
): ByteArray {
    val dh1 = KeyAgreement.dh(aliceIdentity.private, bobIdentityKey)
    val dh2 = KeyAgreement.dh(aliceIdentity.private, bobSignedPreKey)
    val dh3 = KeyAgreement.dh(aliceIdentity.private, bobOneTimePreKey ?: return byteArrayOf())
    // 将三个 DH 结果拼接后做 HKDF 派生
    val sharedSecret = dh1 + dh2 + dh3
    return HKDF.derive(sharedSecret, "X3DH_Shared_Secret".toByteArray(), 32)
}

一个小技巧:一次性预密钥用完后要及时从服务端删除。我见过有 App 忘了清理,导致同一个预密钥被重复使用,前向安全性直接归零。

第三步:消息层——AES-256-GCM 加密

会话密钥协商好了,接下来每条消息都要独立加密。我选 AES-256-GCM,因为它同时提供加密和完整性校验,而且支持随机 Nonce,不需要填充。

// 消息加密——每条消息使用随机 Nonce
fun encryptMessage(plaintext: ByteArray, sessionKey: ByteArray): EncryptedMessage {
    val nonce = SecureRandom().nextBytes(12)  // GCM 推荐 12 字节 Nonce
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(128, nonce)
    cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sessionKey, "AES"), spec)
    val ciphertext = cipher.doFinal(plaintext)
    return EncryptedMessage(nonce, ciphertext)
}

这里要注意,Nonce 绝对不能重复。一旦重复,GCM 的认证标签就失效了。我在项目中用了一个计数器 + 随机数的组合,确保即使在同一毫秒内发送多条消息,Nonce 也不会碰撞。

第四步:应用层——防重放与消息顺序

加密只是第一步。攻击者虽然看不懂消息内容,但他可以把消息截获后重新发送。这就是重放攻击。

怎么防?我给每条消息加一个递增的序号,接收方维护一个滑动窗口,只接受序号大于窗口起始值的消息。同时,每条消息附带一个 HMAC,用会话密钥计算,防止篡改。

// 消息结构——包含序号和 HMAC
data class SecureMessage(
    val sequenceNumber: Long,
    val ciphertext: ByteArray,
    val nonce: ByteArray,
    val hmac: ByteArray
)

fun verifyMessage(msg: SecureMessage, sessionKey: ByteArray): Boolean {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(sessionKey, "HmacSHA256"))
    mac.update(msg.sequenceNumber.toByteArray())
    mac.update(msg.ciphertext)
    mac.update(msg.nonce)
    val expectedHmac = mac.doFinal()
    return MessageDigest.isEqual(expectedHmac, msg.hmac)
}

核心要点总结

  • 传输层用 TLS 1.3 + 证书锁定,防中间人
  • 会话层用 X3DH 协商密钥,保证前向安全
  • 消息层用 AES-256-GCM 独立加密,每条消息随机 Nonce
  • 应用层加序号和 HMAC,防重放防篡改

性能与安全的平衡

你可能会问:这么多层加密,性能扛得住吗?嗯,这里要注意。TLS 1.3 的握手只需要 1-RTT,X3DH 只在首次会话时做一次,后续的消息加密用的是对称加密,速度很快。我在项目中实测,AES-256-GCM 在手机上加密一条 1KB 的消息,耗时不到 0.1 毫秒。

真正耗性能的是密钥协商时的非对称运算。所以我的做法是:App 启动时预加载一批一次性预密钥,避免每次发消息都去服务端拉取。

写在最后

三十章的内容,从证书链到 TLS 握手,从双向认证到证书锁定,再到今天的综合实战。说实话,安全通信这条路没有终点。攻击者在进化,我们的防护手段也要跟着进化。

但我希望这套架构能给你一个扎实的起点。下次有人问你「Android 安全通信怎么做」,你可以自信地告诉他:四层防线,缺一不可。

公众号:蓝海资料掘金营,微信deep3321