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

[Auth] Handle errors #449

Merged
merged 7 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions Example/IntegrationTests/Auth/AuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ final class AuthTests: XCTestCase {
wait(for: [expectation], timeout: 5)
}


func makeClient(prefix: String, account: Account? = nil) -> AuthClient {
let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug)
let relayHost = "relay.walletconnect.com"
Expand Down Expand Up @@ -70,7 +69,7 @@ final class AuthTests: XCTestCase {
Task(priority: .high) {
let signature = try! MessageSigner(signer: Signer()).sign(message: message, privateKey: prvKey)
let cacaoSignature = CacaoSignature(t: "eip191", s: signature)
try! await wallet.respond(.success(RespondParams(id: id, signature: cacaoSignature)))
try! await wallet.respond(requestId: id, result: .success(cacaoSignature))
}
}
.store(in: &publishers)
Expand All @@ -79,6 +78,6 @@ final class AuthTests: XCTestCase {
responseExpectation.fulfill()
}
.store(in: &publishers)
wait(for: [responseExpectation], timeout: 2)
wait(for: [responseExpectation], timeout: 5)
}
}
8 changes: 4 additions & 4 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public class AuthClient {
authRequestPublisherSubject.eraseToAnyPublisher()
}

private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result<Cacao, ErrorCode>), Never>()
public var authResponsePublisher: AnyPublisher<(id: RPCID, result: Result<Cacao, ErrorCode>), Never> {
private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result<Cacao, AuthError>), Never>()
public var authResponsePublisher: AnyPublisher<(id: RPCID, result: Result<Cacao, AuthError>), Never> {
authResponsePublisherSubject.eraseToAnyPublisher()
}

Expand Down Expand Up @@ -87,9 +87,9 @@ public class AuthClient {
try await appRequestService.request(params: params, topic: topic)
}

public func respond(_ result: Result<RespondParams, ErrorCode>) async throws {
public func respond(requestId: RPCID, result: Result<CacaoSignature, Never>) async throws {
guard let account = account else { throw Errors.unknownWalletAddress }
try await walletRespondService.respond(result: result, account: account)
try await walletRespondService.respond(requestId: requestId, result: result, account: account)
}

public func getPendingRequests() throws -> [AuthRequest] {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Auth/AuthClientFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct AuthClientFactory {
let messageSigner = MessageSigner(signer: Signer())
let appRespondSubscriber = AppRespondSubscriber(networkingInteractor: networkingInteractor, logger: logger, rpcHistory: history, signatureVerifier: messageSigner, messageFormatter: messageFormatter)
let walletPairService = WalletPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore)
let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: logger, messageFormatter: messageFormatter, address: account?.address)
let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: logger, kms: kms, messageFormatter: messageFormatter, address: account?.address)
let walletRespondService = WalletRespondService(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history)
let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: history)
let cleanupService = CleanupService(pairingStore: pairingStore, kms: kms)
Expand Down
39 changes: 16 additions & 23 deletions Sources/Auth/Services/App/AppRespondSubscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class AppRespondSubscriber {
private let signatureVerifier: MessageSignatureVerifying
private let messageFormatter: SIWEMessageFormatting
private var publishers = [AnyCancellable]()
var onResponse: ((_ id: RPCID, _ result: Result<Cacao, ErrorCode>) -> Void)?

var onResponse: ((_ id: RPCID, _ result: Result<Cacao, AuthError>) -> Void)?

init(networkingInteractor: NetworkInteracting,
logger: ConsoleLogging,
Expand All @@ -26,7 +27,6 @@ class AppRespondSubscriber {
}

private func subscribeForResponse() {
// TODO - handle error response
networkingInteractor.responsePublisher.sink { [unowned self] subscriptionPayload in
guard
let requestId = subscriptionPayload.response.id,
Expand All @@ -36,30 +36,23 @@ class AppRespondSubscriber {

networkingInteractor.unsubscribe(topic: subscriptionPayload.topic)

do {
guard let cacao = try subscriptionPayload.response.result?.get(Cacao.self) else {
return logger.debug("Malformed auth response params")
}
guard
let cacao = try? subscriptionPayload.response.result?.get(Cacao.self),
let address = try? DIDPKH(iss: cacao.payload.iss).account.address,
let message = try? messageFormatter.formatMessage(from: cacao.payload)
else { self.onResponse?(requestId, .failure(.malformedResponseParams)); return }

guard let requestPayload = try? requestParams.get(AuthRequestParams.self)
else { self.onResponse?(requestId, .failure(.malformedRequestParams)); return }

guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message
else { self.onResponse?(requestId, .failure(.messageCompromised)); return }

let requestPayload = try requestParams.get(AuthRequestParams.self)
let address = try DIDPKH(iss: cacao.payload.iss).account.address
let message = try messageFormatter.formatMessage(from: cacao.payload)
let originalMessage = messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address)
guard let _ = try? signatureVerifier.verify(signature: cacao.signature.s, message: message, address: address)
else { self.onResponse?(requestId, .failure(.messageVerificationFailed)); return }

guard originalMessage == message else {
return logger.debug("Original message compromised")
}
onResponse?(requestId, .success(cacao))

try signatureVerifier.verify(
signature: cacao.signature.s,
message: message,
address: address
)
logger.debug("Received response with valid signature")
onResponse?(requestId, .success(cacao))
} catch {
logger.debug("Received response with invalid signature")
}
}.store(in: &publishers)
}
}
19 changes: 15 additions & 4 deletions Sources/Auth/Services/Common/NetworkingInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ protocol NetworkInteracting {
func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws
func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws
func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws
func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws
}

extension NetworkInteracting {
Expand All @@ -27,14 +28,17 @@ class NetworkingInteractor: NetworkInteracting {
private let serializer: Serializing
private let rpcHistory: RPCHistory
private let logger: ConsoleLogging

private let requestPublisherSubject = PassthroughSubject<RequestSubscriptionPayload, Never>()
var requestPublisher: AnyPublisher<RequestSubscriptionPayload, Never> {
requestPublisherSubject.eraseToAnyPublisher()
}
private let requestPublisherSubject = PassthroughSubject<RequestSubscriptionPayload, Never>()

private let responsePublisherSubject = PassthroughSubject<ResponseSubscriptionPayload, Never>()
var responsePublisher: AnyPublisher<ResponseSubscriptionPayload, Never> {
responsePublisherSubject.eraseToAnyPublisher()
}
private let responsePublisherSubject = PassthroughSubject<ResponseSubscriptionPayload, Never>()

var socketConnectionStatusPublisher: AnyPublisher<SocketConnectionStatus, Never>

init(relayClient: RelayClient,
Expand Down Expand Up @@ -99,11 +103,18 @@ class NetworkingInteractor: NetworkInteracting {
try await relayClient.publish(topic: topic, payload: message, tag: tag)
}

func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws {
let error = JSONRPCError(code: reason.code, message: reason.message)
let response = RPCResponse(id: requestId, error: error)
let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType)
try await relayClient.publish(topic: topic, payload: message, tag: tag)
}

private func manageSubscription(_ topic: String, _ encodedEnvelope: String) {
if let deserializedJsonRpcRequest: RPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) {
handleRequest(topic: topic, request: deserializedJsonRpcRequest)
} else if let deserializedJsonRpcResponse: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) {
handleResponse(response: deserializedJsonRpcResponse)
} else if let response: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) {
handleResponse(response: response)
} else {
logger.debug("Networking Interactor - Received unknown object type from networking relay")
}
Expand Down
48 changes: 29 additions & 19 deletions Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,58 @@ import Combine
import Foundation
import WalletConnectUtils
import JSONRPC
import WalletConnectKMS

class WalletRequestSubscriber {
private let networkingInteractor: NetworkInteracting
private let logger: ConsoleLogging
private let kms: KeyManagementServiceProtocol
private let address: String?
private var publishers = [AnyCancellable]()
private let messageFormatter: SIWEMessageFormatting
var onRequest: ((_ id: RPCID, _ message: String) -> Void)?

init(networkingInteractor: NetworkInteracting,
logger: ConsoleLogging,
kms: KeyManagementServiceProtocol,
messageFormatter: SIWEMessageFormatting,
address: String?) {
self.networkingInteractor = networkingInteractor
self.logger = logger
self.kms = kms
self.address = address
self.messageFormatter = messageFormatter
subscribeForRequest()
}

private func subscribeForRequest() {
guard let address = address else {return}
networkingInteractor.requestPublisher.sink { [unowned self] subscriptionPayload in
guard let address = address else { return }

networkingInteractor.requestPublisher.sink { [unowned self] payload in

logger.debug("WalletRequestSubscriber: Received request")
guard
let requestId = subscriptionPayload.request.id,
subscriptionPayload.request.method == "wc_authRequest" else { return }

do {
guard let authRequestParams = try subscriptionPayload.request.params?.get(AuthRequestParams.self) else { return logger.debug("Malformed auth request params")
}

let message = messageFormatter.formatMessage(
from: authRequestParams.payloadParams,
address: address
)

onRequest?(requestId, message)
} catch {
logger.debug(error)
}

guard let requestId = payload.request.id, payload.request.method == "wc_authRequest"
else { return }

guard let authRequestParams = try? payload.request.params?.get(AuthRequestParams.self)
else { return respondError(.malformedRequestParams, topic: payload.topic, requestId: requestId) }

let message = messageFormatter.formatMessage(from: authRequestParams.payloadParams, address: address)

onRequest?(requestId, message)
}.store(in: &publishers)
}

private func respondError(_ error: AuthError, topic: String, requestId: RPCID) {
guard let pubKey = kms.getAgreementSecret(for: topic)?.publicKey
else { return logger.error("Agreement key for topic \(topic) not found") }

let tag = AuthResponseParams.tag
let envelopeType = Envelope.EnvelopeType.type1(pubKey: pubKey.rawRepresentation)

Task(priority: .high) {
try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType)
}
}
}
59 changes: 42 additions & 17 deletions Sources/Auth/Services/Wallet/WalletRespondService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,55 @@ actor WalletRespondService {
self.kms = kms
self.rpcHistory = rpcHistory
}
func respond(result: Result<RespondParams, ErrorCode>, account: Account) async throws {

func respond(requestId: RPCID, result: Result<CacaoSignature, Never>, account: Account) async throws {
switch result {
case .success(let params):
try await respond(respondParams: params, account: account)
case .failure(let error):
fatalError("TODO respond with error")
case .success(let signature):
try await respond(requestId: requestId, signature: signature, account: account)
case .failure:
try await respondError(requestId: requestId)
}
}

private func respond(respondParams: RespondParams, account: Account) async throws {
guard let request = rpcHistory.get(recordId: respondParams.id)?.request else { throw Errors.recordForIdNotFound }
guard let authRequestParams = try? request.params?.get(AuthRequestParams.self) else { throw Errors.malformedAuthRequestParams }
private func respond(requestId: RPCID, signature: CacaoSignature, account: Account) async throws {
let authRequestParams = try getAuthRequestParams(requestId: requestId)
let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams)

let peerPubKey = try AgreementPublicKey(hex: authRequestParams.requester.publicKey)
let responseTopic = peerPubKey.rawRepresentation.sha256().toHexString()
let selfPubKey = try kms.createX25519KeyPair()
let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.hexRepresentation)
try kms.setAgreementSecret(agreementKeys, topic: responseTopic)
try kms.setAgreementSecret(keys, topic: topic)

let didpkh = DIDPKH(account: account)
let cacao = CacaoFormatter().format(authRequestParams, respondParams.signature, didpkh)
let response = RPCResponse(id: request.id!, result: cacao)
let cacao = CacaoFormatter().format(authRequestParams, signature, didpkh)
let response = RPCResponse(id: requestId, result: cacao)
try await networkingInteractor.respond(topic: topic, response: response, tag: AuthResponseParams.tag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation))
}

private func respondError(requestId: RPCID) async throws {
let authRequestParams = try getAuthRequestParams(requestId: requestId)
let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams)

try kms.setAgreementSecret(keys, topic: topic)

let tag = AuthResponseParams.tag
let error = AuthError.userRejeted
let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation)
try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType)
}

private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams {
guard let request = rpcHistory.get(recordId: requestId)?.request
else { throw Errors.recordForIdNotFound }

guard let authRequestParams = try request.params?.get(AuthRequestParams.self)
else { throw Errors.malformedAuthRequestParams }

try await networkingInteractor.respond(topic: responseTopic, response: response, tag: AuthResponseParams.tag, envelopeType: .type1(pubKey: selfPubKey.rawRepresentation))
return authRequestParams
}

private func generateAgreementKeys(requestParams: AuthRequestParams) throws -> (topic: String, keys: AgreementKeys) {
let peerPubKey = try AgreementPublicKey(hex: requestParams.requester.publicKey)
let topic = peerPubKey.rawRepresentation.sha256().toHexString()
let selfPubKey = try kms.createX25519KeyPair()
let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.hexRepresentation)
return (topic, keys)
}
}
8 changes: 4 additions & 4 deletions Sources/Auth/Types/Cacao/CacaoSignature.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

struct CacaoSignature: Codable, Equatable {
let t: String
let s: String
let m: String? = nil
public struct CacaoSignature: Codable, Equatable {
public let t: String
public let s: String
public let m: String? = nil
}
7 changes: 0 additions & 7 deletions Sources/Auth/Types/ErrorCode.swift

This file was deleted.

42 changes: 42 additions & 0 deletions Sources/Auth/Types/Errors/AuthError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

public enum AuthError: Codable, Equatable, Error {
case userRejeted
case malformedResponseParams
case malformedRequestParams
case messageCompromised
case messageVerificationFailed
}

extension AuthError: Reason {

public var code: Int {
switch self {
case .userRejeted:
return 14001
case .malformedResponseParams:
return 12001
case .malformedRequestParams:
return 12002
case .messageCompromised:
return 12003
case .messageVerificationFailed:
return 12004
}
}

public var message: String {
switch self {
case .userRejeted:
return "Auth request rejected by user"
case .malformedResponseParams:
return "Response params malformed"
case .malformedRequestParams:
return "Request params malformed"
case .messageCompromised:
return "Original message compromised"
case .messageVerificationFailed:
return "Message verification failed"
}
}
}
7 changes: 7 additions & 0 deletions Sources/Auth/Types/Errors/Reason.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

protocol Reason {
var code: Int { get }
var message: String { get }
}

Loading