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 6 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
4 changes: 2 additions & 2 deletions Example/IntegrationTests/Auth/AuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,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 +79,6 @@ final class AuthTests: XCTestCase {
responseExpectation.fulfill()
}
.store(in: &publishers)
wait(for: [responseExpectation], timeout: 2)
wait(for: [responseExpectation], timeout: .infinity)
flypaper0 marked this conversation as resolved.
Show resolved Hide resolved
}
}
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, InternalError>), Never>()
public var authResponsePublisher: AnyPublisher<(id: RPCID, result: Result<Cacao, InternalError>), Never> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the error can be a protocol error.
consider case when wallet rejects a request - user initiated

Copy link
Contributor

Choose a reason for hiding this comment

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

so if it can be InternalError and ExternalError maybe we could have only one public error like: AuthError?

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, ExternalError>) async throws {
Copy link
Contributor

@llbartekll llbartekll Aug 19, 2022

Choose a reason for hiding this comment

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

I think here it should be a protocol error as well, as it is send to a peer

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, InternalError>) -> 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: InternalError, topic: String, requestId: RPCID) {
Copy link
Contributor

Choose a reason for hiding this comment

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

would it make sense if this method is async?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is no async context. I think it will be the same

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)
}
}
}
56 changes: 40 additions & 16 deletions Sources/Auth/Services/Wallet/WalletRespondService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,54 @@ 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, ExternalError>, account: Account) async throws {
switch result {
case .success(let params):
try await respond(respondParams: params, account: account)
case .success(let signature):
try await respond(requestId: requestId, signature: signature, account: account)
case .failure(let error):
fatalError("TODO respond with error")
try await respond(error: error, 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 respond(error: ExternalError, 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 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
}
22 changes: 22 additions & 0 deletions Sources/Auth/Types/Error/ExternalError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

public enum ExternalError: Codable, Equatable, Error {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe PeerError?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My suggestion: ExternalError - errors which comes from outside. InternalError - errors which could occur during SDK execution

case userRejeted
}

extension ExternalError: Reason {

public var code: Int {
switch self {
case .userRejeted:
return 2001
}
}

public var message: String {
switch self {
case .userRejeted:
return "Auth request rejected by user"
}
}
}
37 changes: 37 additions & 0 deletions Sources/Auth/Types/Error/InternalError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

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

extension InternalError: Reason {

public var code: Int {
switch self {
case .malformedResponseParams:
return 1001
case .malformedRequestParams:
return 1002
case .messageCompromised:
return 1003
case .messageVerificationFailed:
return 1004
}
}

public var message: String {
switch self {
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/Error/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 }
}

7 changes: 0 additions & 7 deletions Sources/Auth/Types/ErrorCode.swift

This file was deleted.

Loading