Web3 学习日记 - Wallet Connect 分析 (四)
DApp 和 Wallet 的通讯数据处理
Envelope
WalletConnect 将数据封装方式定义为 Envelope https://github.com/WalletConnect/WalletConnectSwiftV2/blob/develop/Sources/WalletConnectKMS/Serialiser/Envelope.swift
一共有三种 type
public extension Envelope {
enum EnvelopeType: Equatable {
/// type 0 = tp + iv + ct + tag
case type0
/// type 1 = tp + pk + iv + ct + tag - base64encoded
case type1(pubKey: Data) //注意这里 type1需要携带发送方的pubKey
/// type 2 = tp + base64urlEncoded unencrypted string
case type2
}
...
}
编码处理
对各种 Envelope 的编码处理如下
/// Encrypts and serializes an object
/// - Parameters:
/// - topic: Topic that is associated with a symetric key for encrypting particular codable object
/// - encodable: Object to encrypt and serialize
/// - envelopeType: type of envelope
/// - Returns: Serialized String
public func serialize(topic: String?, encodable: Encodable, envelopeType: Envelope.EnvelopeType, codingType: Envelope.CodingType) throws -> String {
if envelopeType == .type2 {
return try serializeEnvelopeType2(encodable: encodable, codingType: codingType)
}
guard let topic = topic else {
let error = Errors.topicNotFound
logger.error("\(error)")
throw error
}
let messageJson = try encodable.json()
guard let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) else {
let error = Errors.symmetricKeyForTopicNotFound(topic)
logger.error("\(error)")
throw error
}
let sealbox = try codec.encode(plaintext: messageJson, symmetricKey: symmetricKey)
return Envelope(type: envelopeType, sealbox: sealbox, codingType: codingType).serialised(codingType: codingType)
}
可以看到, 编码的时候除了 type2 是明文的, type0 和 type1 都是从 topic 中取出对称加密密钥来对 message 进行加密。 当然 type0 和 type1 在解码时的操作也会有点不同。下面会讲到。
解码
对各种 Envelope 的解码处理如下
/// Deserializes and decrypts an object
/// - Parameters:
/// - topic: Topic that is associated with a symetric key for decrypting particular codable object
/// - encodedEnvelope: Envelope to deserialize and decrypt
/// - Returns: Deserialized object
public func deserialize<T: Codable>(topic: String, codingType: Envelope.CodingType, envelopeString: String) throws -> (T, derivedTopic: String?, decryptedPayload: Data) {
let envelope = try Envelope(codingType, envelopeString: envelopeString)
switch envelope.type {
case .type0:
let deserialisedType: (object: T, data: Data) = try handleType0Envelope(topic, envelope)
return (deserialisedType.object, nil, deserialisedType.data)
case .type1(let peerPubKey):
return try handleType1Envelope(topic, peerPubKey: peerPubKey, sealbox: envelope.sealbox)
case .type2:
let decodedType: T = try handleType2Envelope(envelope: envelope)
return (decodedType, nil, Data())
}
}
下面我们详细看看每种 Envelope type 的解码细节
type2
先看最简单的 type2 处理, 明文传递, 直接 decode 为对应对象 https://github.com/WalletConnect/WalletConnectSwiftV2/blob/8c7e413c6f451304235ed161085ddc7947e0e6ae/Sources/WalletConnectKMS/Serialiser/Serializer.swift#L133-L141
private func handleType2Envelope<T: Codable>(envelope: Envelope) throws -> T {
do {
let deserialised = try JSONDecoder().decode(T.self, from: envelope.sealbox)
return deserialised
} catch {
logger.error(error)
throw error
}
}
Envelope type2 目前在 link mode 的 wc_sessionAuthenticate 的 request 里用到(DApp -> Wallet)
type0
type0 采用了对称加密密钥,而且密钥是之前已经保存在了本地 keychain 里的 (使用 topic 作为 key)
从某个 topic 收到 message 之后,会以 topic 为 key 去 keychain 里查询出对称密钥, 并用密钥去解密 message
+-------+ key value +---------+
| topic | -----------------> | 对称密钥 |
+-------+ +---------+
private func handleType0Envelope<T: Codable>(_ topic: String, _ envelope: Envelope) throws -> (T, Data) {
if let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) {
do {
let decoded: (T, Data) = try decode(sealbox: envelope.sealbox, symmetricKey: symmetricKey)
logger.debug("Decoded: \(decoded.0)")
return decoded
}
catch {
logger.debug("\(error)")
throw error
}
} else {
let error = Errors.symmetricKeyForTopicNotFound(topic)
logger.error("\(error)")
throw error
}
}
Envelope type0 被大多 rpc method 使用。
type1
type1 是以 topic 为 key 在本地 keychain 里保存了一个 ed25519 的公钥
从某个 topic 收到 message 之后,会以 topic 为 key 去 keychain 里查询出 ed25519 公钥 然后这个公钥作为 key, 可以取到这个 ed25519 的私钥。
也就是说 type1 商量好 agreementKeys 之后,会转为 type 0 的模式
(私钥产生公钥。公钥 hash 生成 topic。 公钥作为 key 可以读取到私钥, topic 作为 key 可以读取到公钥)
hash
+----------------------+
v |
+-------+ key value +-----------+ key value +------------+
| topic | -----------> | PublicKey | -----------> | PrivateKey |
+-------+ +-----------+ +------------+
^ generate |
+--------------------------+
然后从 type1 的定义看,里面是包含了 peerPubKey 的
public extension Envelope {
enum EnvelopeType: Equatable {
/// type 0 = tp + iv + ct + tag
case type0
/// type 1 = tp + pk + iv + ct + tag - base64encoded
case type1(pubKey: Data) //注意这里 type1需要携带发送方的pubKey
/// type 2 = tp + base64urlEncoded unencrypted string
case type2
}
...
}
此时可以用自己的 PrivateKey 和 peerPubKey,使用 ECDHE 协商出一个 agreementKeys (双方生成的是一样的)
然后使用 agreementKeys 来解密 type1 的 message.
private func handleType1Envelope<T: Codable>(_ topic: String, peerPubKey: Data, sealbox: Data) throws -> (T, String, Data) {
guard let selfPubKey = kms.getPublicKey(for: topic)
else {
let error = Errors.publicKeyForTopicNotFound
logger.error("\(error)")
throw error
}
let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.toHexString())
let decodedType: (object: T, data: Data) = try decode(sealbox: sealbox, symmetricKey: agreementKeys.sharedKey.rawRepresentation)
let derivedTopic = agreementKeys.derivedTopic()
try kms.setAgreementSecret(agreementKeys, topic: derivedTopic)
return (decodedType.object, derivedTopic, decodedType.data)
}
Envelope type1 目前在 relay/link mode 的 wc_sessionAuthenticate 的 response 里用到(Wallet -> DApp) https://github.com/WalletConnect/WalletConnectSwiftV2/blob/8c7e413c6f451304235ed161085ddc7947e0e6ae/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift#L57-L62