Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RPCRequest] Replay protection #1143

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
23 changes: 2 additions & 21 deletions Example/DApp/Sign/SelectChain/SelectChainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,13 @@ class SelectChainViewController: UIViewController, UITableViewDataSource {
let namespaces: [String: ProposalNamespace] = [
"eip155": ProposalNamespace(
chains: [
Blockchain("eip155:137")!
Blockchain("eip155:1")!
],
methods: [
"eth_sendTransaction",
"personal_sign",
"eth_signTypedData"
], events: []
),
"eip155:1": ProposalNamespace(
methods: [
"eth_sendTransaction",
"personal_sign",
"eth_signTypedData"
],
events: []
)
]
let optionalNamespaces: [String: ProposalNamespace] = [
"solana": ProposalNamespace(
chains: [
Blockchain("solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")!
],
methods: [
"solana_signMessage",
"solana_signTransaction"
], events: []
)
]
let sessionProperties: [String: String] = [
Expand All @@ -73,7 +54,7 @@ class SelectChainViewController: UIViewController, UITableViewDataSource {
Task {
WalletConnectModal.set(sessionParams: .init(
requiredNamespaces: namespaces,
optionalNamespaces: optionalNamespaces,
optionalNamespaces: [:],
sessionProperties: sessionProperties
))
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Auth/Services/App/AppRequestService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ actor AppRequestService {
let requester = AuthRequestParams.Requester(publicKey: pubKey.hexRepresentation, metadata: appMetadata)
let payload = AuthPayload(requestParams: params, iat: iatProvader.iat)
let params = AuthRequestParams(requester: requester, payloadParams: payload)
let request = RPCRequest(method: "wc_authRequest", params: params)
let request = RPCRequest(method: "wc_authRequest", params: params, topic: topic)
try kms.setPublicKey(publicKey: pubKey, for: responseTopic)
logger.debug("AppRequestService: Subscribibg for response topic: \(responseTopic)")
try await networkingInteractor.request(request, topic: topic, protocolMethod: AuthRequestProtocolMethod())
Expand Down
1 change: 1 addition & 0 deletions Sources/Chat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public class ChatClient {
/// Ping its peer to evaluate if it's currently online
/// - Parameter topic: chat thread topic
public func ping(topic: String) {
// Pass 'topic' value to the RPCRequest when implemented
fatalError("not implemented")
}

Expand Down
1 change: 1 addition & 0 deletions Sources/Chat/ProtocolServices/Common/LeaveService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation

class LeaveService {
func leave(topic: String) async throws {
// Pass 'topic' value to the RPCRequest when implemented
fatalError("not implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class MessagingService {
)

let protocolMethod = ChatMessageProtocolMethod()
let request = RPCRequest(method: protocolMethod.method, params: wrapper)
let request = RPCRequest(method: protocolMethod.method, params: wrapper, topic: topic)
try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod)

logger.debug("Message sent on topic: \(topic)")
Expand Down
2 changes: 1 addition & 1 deletion Sources/Chat/ProtocolServices/Inviter/InviteService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class InviteService {
)

let inviteId = RPCID()
let request = RPCRequest(method: protocolMethod.method, params: wrapper, rpcid: inviteId)
let request = RPCRequest(method: protocolMethod.method, params: wrapper, rpcid: inviteId, topic: inviteTopic)

try await networkingInteractor.subscribe(topic: responseTopic)
try await networkingInteractor.request(request, topic: inviteTopic, protocolMethod: protocolMethod, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation))
Expand Down
50 changes: 27 additions & 23 deletions Sources/JSONRPC/RPCRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,59 +16,62 @@ public struct RPCRequest: Equatable {
public let params: AnyCodable?

public let id: RPCID?

public let topic: String?

internal init(method: String, params: AnyCodable?, id: RPCID?) {
internal init(method: String, params: AnyCodable?, id: RPCID?, topic: String? = nil) {
self.jsonrpc = "2.0"
self.method = method
self.params = params
self.id = id
self.topic = topic
}

internal init<C>(method: String, checkedParams params: C, id: RPCID) throws where C: Codable {
internal init<C>(method: String, checkedParams params: C, id: RPCID, topic: String?) throws where C: Codable {
if params is Int || params is Double || params is String || params is Bool {
throw Error.invalidPrimitiveParameter
}
self.init(method: method, params: AnyCodable(params), id: id)
self.init(method: method, params: AnyCodable(params), id: id, topic: topic)
}

public init<C>(method: String, checkedParams params: C, idGenerator: IdentifierGenerator = defaultIdentifierGenerator) throws where C: Codable {
try self.init(method: method, checkedParams: params, id: idGenerator.next())
public init<C>(method: String, checkedParams params: C, idGenerator: IdentifierGenerator = defaultIdentifierGenerator, topic: String?) throws where C: Codable {
try self.init(method: method, checkedParams: params, id: idGenerator.next(), topic: topic)
}

public init<C>(method: String, checkedParams params: C, id: Int64) throws where C: Codable {
try self.init(method: method, checkedParams: params, id: .right(id))
public init<C>(method: String, checkedParams params: C, id: Int64, topic: String?) throws where C: Codable {
try self.init(method: method, checkedParams: params, id: .right(id), topic: topic)
}

public init<C>(method: String, checkedParams params: C, id: String) throws where C: Codable {
try self.init(method: method, checkedParams: params, id: .left(id))
public init<C>(method: String, checkedParams params: C, id: String, topic: String?) throws where C: Codable {
try self.init(method: method, checkedParams: params, id: .left(id), topic: topic)
}

public init<C>(method: String, params: C, idGenerator: IdentifierGenerator = defaultIdentifierGenerator) where C: Codable {
self.init(method: method, params: AnyCodable(params), id: idGenerator.next())
public init<C>(method: String, params: C, idGenerator: IdentifierGenerator = defaultIdentifierGenerator, topic: String?) where C: Codable {
self.init(method: method, params: AnyCodable(params), id: idGenerator.next(), topic: topic)
}

public init<C>(method: String, params: C, id: Int64) where C: Codable {
self.init(method: method, params: AnyCodable(params), id: .right(id))
public init<C>(method: String, params: C, id: Int64, topic: String?) where C: Codable {
self.init(method: method, params: AnyCodable(params), id: .right(id), topic: topic)
}

public init<C>(method: String, params: C, rpcid: RPCID) where C: Codable {
self.init(method: method, params: AnyCodable(params), id: rpcid)
public init<C>(method: String, params: C, rpcid: RPCID, topic: String?) where C: Codable {
self.init(method: method, params: AnyCodable(params), id: rpcid, topic: topic)
}

public init<C>(method: String, params: C, id: String) where C: Codable {
self.init(method: method, params: AnyCodable(params), id: .left(id))
public init<C>(method: String, params: C, id: String, topic: String?) where C: Codable {
self.init(method: method, params: AnyCodable(params), id: .left(id), topic: topic)
}

public init(method: String, idGenerator: IdentifierGenerator = defaultIdentifierGenerator) {
self.init(method: method, params: nil, id: idGenerator.next())
public init(method: String, idGenerator: IdentifierGenerator = defaultIdentifierGenerator, topic: String?) {
self.init(method: method, params: nil, id: idGenerator.next(), topic: topic)
}

public init(method: String, id: Int64) {
self.init(method: method, params: nil, id: .right(id))
public init(method: String, id: Int64, topic: String?) {
self.init(method: method, params: nil, id: .right(id), topic: topic)
}

public init(method: String, id: String) {
self.init(method: method, params: nil, id: .left(id))
public init(method: String, id: String, topic: String?) {
self.init(method: method, params: nil, id: .left(id), topic: topic)
}
}

Expand Down Expand Up @@ -101,6 +104,7 @@ extension RPCRequest: Codable {
id = try container.decodeIfPresent(RPCID.self, forKey: .id)
method = try container.decode(String.self, forKey: .method)
params = try container.decodeIfPresent(AnyCodable.self, forKey: .params)
topic = try container.decodeIfPresent(String.self, forKey: .topic)
if let decodedParams = params {
if decodedParams.value is Int || decodedParams.value is Double || decodedParams.value is String || decodedParams.value is Bool {
throw DecodingError.dataCorruptedError(
Expand Down
10 changes: 9 additions & 1 deletion Sources/WalletConnectNetworking/NetworkingInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,15 @@ public class NetworkingInteractor: NetworkInteracting {

private func manageSubscription(_ topic: String, _ encodedEnvelope: String, _ publishedAt: Date) {
if let (deserializedJsonRpcRequest, derivedTopic, decryptedPayload): (RPCRequest, String?, Data) = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) {
handleRequest(topic: topic, request: deserializedJsonRpcRequest, decryptedPayload: decryptedPayload, publishedAt: publishedAt, derivedTopic: derivedTopic)
if let rpcRequestTopic = deserializedJsonRpcRequest.topic {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if the subscription topic matches the request's topic.
If there is no request topic - the request will be handled to support backward compatibility.

if rpcRequestTopic == topic {
handleRequest(topic: topic, request: deserializedJsonRpcRequest, decryptedPayload: decryptedPayload, publishedAt: publishedAt, derivedTopic: derivedTopic)
} else {
logger.debug("Networking Interactor - Mismatched topic decoded from message")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we log it as an error or warning?

}
} else {
handleRequest(topic: topic, request: deserializedJsonRpcRequest, decryptedPayload: decryptedPayload, publishedAt: publishedAt, derivedTopic: derivedTopic)
}
} else if let (response, derivedTopic, _): (RPCResponse, String?, Data) = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) {
handleResponse(topic: topic, response: response, publishedAt: publishedAt, derivedTopic: derivedTopic)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class DeleteNotifySubscriptionRequester {
account: subscription.account
)

let request = RPCRequest(method: protocolMethod.method, params: wrapper)
let request = RPCRequest(method: protocolMethod.method, params: wrapper, topic: topic)
try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod)

try notifyStorage.deleteSubscription(topic: topic)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ class NotifyUpdateRequester: NotifyUpdateRequesting {
let request = try createJWTRequest(
dappPubKey: DIDKey(rawData: dappAuthenticationKey),
subscriptionAccount: subscription.account,
appDomain: subscription.metadata.url, scope: scope
appDomain: subscription.metadata.url, scope: scope,
topic: topic
)

let protocolMethod = NotifyUpdateProtocolMethod()

try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod)
}

private func createJWTRequest(dappPubKey: DIDKey, subscriptionAccount: Account, appDomain: String, scope: Set<String>) throws -> RPCRequest {
private func createJWTRequest(dappPubKey: DIDKey, subscriptionAccount: Account, appDomain: String, scope: Set<String>, topic: String) throws -> RPCRequest {
let protocolMethod = NotifyUpdateProtocolMethod().method
let scopeClaim = scope.joined(separator: " ")
let app = DIDWeb(host: appDomain)
Expand All @@ -62,6 +63,6 @@ class NotifyUpdateRequester: NotifyUpdateRequesting {
payload: jwtPayload,
account: subscriptionAccount
)
return RPCRequest(method: protocolMethod, params: wrapper)
return RPCRequest(method: protocolMethod, params: wrapper, topic: topic)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class NotifyWatchSubscriptionsRequester {
subscriptionAccount: account)


let request = RPCRequest(method: protocolMethod.method, params: watchSubscriptionsAuthWrapper)
let request = RPCRequest(method: protocolMethod.method, params: watchSubscriptionsAuthWrapper, topic: responseTopic)

logger.debug("Subscribing to response topic: \(responseTopic)")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class NotifySubscribeRequester {
subscriptionAccount: account,
appDomain: appDomain
)
let request = RPCRequest(method: protocolMethod.method, params: subscriptionAuthWrapper)
let request = RPCRequest(method: protocolMethod.method, params: subscriptionAuthWrapper, topic: responseTopic)

logger.debug("Subscribing to response topic: \(responseTopic)")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class DeletePairingService {
let reason = PairingReasonCode.userDisconnected
let protocolMethod = PairingProtocolMethod.delete
logger.debug("Will delete pairing for reason: message: \(reason.message) code: \(reason.code)")
let request = RPCRequest(method: protocolMethod.method, params: reason)
let request = RPCRequest(method: protocolMethod.method, params: reason, topic: topic)
try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod)
pairingStorage.delete(topic: topic)
kms.deleteSymmetricKey(for: topic)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class PingRequester {
}

public func ping(topic: String) async throws {
let request = RPCRequest(method: method.method, params: PairingPingParams())
let request = RPCRequest(method: method.method, params: PairingPingParams(), topic: topic)
try await networkingInteractor.request(request, topic: topic, protocolMethod: method)
}
}
2 changes: 1 addition & 1 deletion Sources/WalletConnectRelay/RPC/RelayRPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension RelayRPC where Parameters: Codable {
}

func asRPCRequest() -> RPCRequest {
RPCRequest(method: self.method, params: self.params, idGenerator: self.idGenerator)
RPCRequest(method: self.method, params: self.params, idGenerator: self.idGenerator, topic: nil)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ final class ApproveEngine {
sessionStore.setSession(session)

let protocolMethod = SessionSettleProtocolMethod()
let request = RPCRequest(method: protocolMethod.method, params: settleParams)
let request = RPCRequest(method: protocolMethod.method, params: settleParams, topic: topic)

async let subscription: () = networkingInteractor.subscribe(topic: topic)
async let settleRequest: () = networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class DeleteSessionService {
let protocolMethod = SessionDeleteProtocolMethod()
let reason = SessionType.Reason(code: reasonCode.code, message: reasonCode.message)
logger.debug("Will delete session for reason: message: \(reason.message) code: \(reason.code)")
let request = RPCRequest(method: protocolMethod.method, params: reason)
let request = RPCRequest(method: protocolMethod.method, params: reason, topic: topic)
try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod)
sessionStore.delete(topic: topic)
logger.debug("Session disconnected")
Expand Down
4 changes: 2 additions & 2 deletions Sources/WalletConnectSign/Engine/Common/SessionEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ final class SessionEngine {
let chainRequest = SessionType.RequestParams.Request(method: request.method, params: request.params, expiry: request.expiry)
let sessionRequestParams = SessionType.RequestParams(request: chainRequest, chainId: request.chainId)
let protocolMethod = SessionRequestProtocolMethod(ttl: request.calculateTtl())
let rpcRequest = RPCRequest(method: protocolMethod.method, params: sessionRequestParams, rpcid: request.id)
let rpcRequest = RPCRequest(method: protocolMethod.method, params: sessionRequestParams, rpcid: request.id, topic: request.topic)
try await networkingInteractor.request(rpcRequest, topic: request.topic, protocolMethod: SessionRequestProtocolMethod())
}

Expand Down Expand Up @@ -106,7 +106,7 @@ final class SessionEngine {
guard session.hasPermission(forEvent: event.name, onChain: chainId) else {
throw WalletConnectError.invalidEvent
}
let rpcRequest = RPCRequest(method: protocolMethod.method, params: SessionType.EventParams(event: event, chainId: chainId))
let rpcRequest = RPCRequest(method: protocolMethod.method, params: SessionType.EventParams(event: event, chainId: chainId), topic: topic)
try await networkingInteractor.request(rpcRequest, topic: topic, protocolMethod: protocolMethod)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class ControllerSessionStateMachine {
try Namespace.validate(namespaces)
logger.debug("Controller will update methods")
sessionStore.setSession(session)
let request = RPCRequest(method: protocolMethod.method, params: SessionType.UpdateParams(namespaces: namespaces))
let request = RPCRequest(method: protocolMethod.method, params: SessionType.UpdateParams(namespaces: namespaces), topic: topic)
try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod)
}

Expand All @@ -42,7 +42,7 @@ final class ControllerSessionStateMachine {
try session.updateExpiry(by: ttl)
let newExpiry = Int64(session.expiryDate.timeIntervalSince1970 )
sessionStore.setSession(session)
let request = RPCRequest(method: protocolMethod.method, params: SessionType.UpdateExpiryParams(expiry: newExpiry))
let request = RPCRequest(method: protocolMethod.method, params: SessionType.UpdateExpiryParams(expiry: newExpiry), topic: topic)
try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ final class AppProposeService {
sessionProperties: sessionProperties
)

let request = RPCRequest(method: protocolMethod.method, params: proposal)
let request = RPCRequest(method: protocolMethod.method, params: proposal, topic: pairingTopic)
try await networkingInteractor.request(request, topic: pairingTopic, protocolMethod: protocolMethod)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ actor EIP1271Verifier {
let encoder = ValidSignatureMethod(signature: signature, messageHash: messageHash)
let call = EthCall(to: address, data: encoder.encode())
let params = AnyCodable([AnyCodable(call), AnyCodable("latest")])
let request = RPCRequest(method: "eth_call", params: params)
let request = RPCRequest(method: "eth_call", params: params, topic: nil)
let data = try JSONEncoder().encode(request)
let httpService = RPCService(data: data, projectId: projectId, chainId: chainId)
let response = try await httpClient.request(RPCResponse.self, at: httpService)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ actor ENSRegistryContract {
let encoder = ENSResolverMethod(namehash: namehash)
let call = EthCall(to: address, data: encoder.encode())
let params = AnyCodable([AnyCodable(call), AnyCodable("latest")])
let request = RPCRequest(method: "eth_call", params: params)
let request = RPCRequest(method: "eth_call", params: params, topic: nil)
let data = try JSONEncoder().encode(request)
let httpService = RPCService(data: data, projectId: projectId, chainId: chainId)
let response = try await httpClient.request(RPCResponse.self, at: httpService)
Expand Down
Loading
Loading