diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectJWT.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectJWT.xcscheme new file mode 100644 index 000000000..0bfec5ddc --- /dev/null +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectJWT.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSigner.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSigner.xcscheme new file mode 100644 index 000000000..7d2425bad --- /dev/null +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSigner.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 8bede13a1..436091772 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -83,7 +83,8 @@ final class AuthTests: XCTestCase { Task(priority: .high) { let signerFactory = DefaultSignerFactory() let signer = MessageSignerFactory(signerFactory: signerFactory).create(projectId: InputConfig.projectId) - let signature = try! signer.sign(payload: request.payload, address: walletAccount.address, privateKey: prvKey, type: .eip191) + let payload = try! request.payload.cacaoPayload(address: walletAccount.address) + let signature = try! signer.sign(payload: payload, privateKey: prvKey, type: .eip191) try! await walletAuthClient.respond(requestId: request.id, signature: signature, from: walletAccount) } } diff --git a/Example/IntegrationTests/Auth/Signer/CacaoSignerTests.swift b/Example/IntegrationTests/Auth/Signer/CacaoSignerTests.swift index c6481015b..8066c147d 100644 --- a/Example/IntegrationTests/Auth/Signer/CacaoSignerTests.swift +++ b/Example/IntegrationTests/Auth/Signer/CacaoSignerTests.swift @@ -45,12 +45,10 @@ class CacaoSignerTest: XCTestCase { func testCacaoSign() throws { let address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - let formatted = try SIWEMessageFormatter().formatMessage( - from: payload, - address: address - ) + let cacaoPayload = try payload.cacaoPayload(address: address) + let formatted = try SIWECacaoFormatter().formatMessage(from: cacaoPayload) XCTAssertEqual(formatted, message) - XCTAssertEqual(try signer.sign(payload: payload, address: address, privateKey: privateKey, type: .eip191), signature) + XCTAssertEqual(try signer.sign(payload: cacaoPayload, privateKey: privateKey, type: .eip191), signature) } func testCacaoVerify() async throws { diff --git a/Example/IntegrationTests/Auth/Signer/EIP1271VerifierTests.swift b/Example/IntegrationTests/Auth/Signer/EIP1271VerifierTests.swift index 45c5c1bdd..40b66bcb4 100644 --- a/Example/IntegrationTests/Auth/Signer/EIP1271VerifierTests.swift +++ b/Example/IntegrationTests/Auth/Signer/EIP1271VerifierTests.swift @@ -1,6 +1,7 @@ import Foundation import XCTest @testable import Auth +@testable import WalletConnectSigner import JSONRPC class EIP1271VerifierTests: XCTestCase { diff --git a/Example/IntegrationTests/Auth/Signer/EIP191VerifierTests.swift b/Example/IntegrationTests/Auth/Signer/EIP191VerifierTests.swift index 2e5c9c1f3..5d5aa2c25 100644 --- a/Example/IntegrationTests/Auth/Signer/EIP191VerifierTests.swift +++ b/Example/IntegrationTests/Auth/Signer/EIP191VerifierTests.swift @@ -1,6 +1,7 @@ import Foundation import XCTest @testable import Auth +@testable import WalletConnectSigner class EIP191VerifierTests: XCTestCase { diff --git a/Example/IntegrationTests/Chat/RegistryTests.swift b/Example/IntegrationTests/Chat/RegistryTests.swift index ea21c9375..e2f58588c 100644 --- a/Example/IntegrationTests/Chat/RegistryTests.swift +++ b/Example/IntegrationTests/Chat/RegistryTests.swift @@ -6,4 +6,50 @@ import WalletConnectUtils final class RegistryTests: XCTestCase { + let account = Account("eip155:1:0x15bca56b6e2728aec2532df9d436bd1600e86688")! + let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") + + var sut: IdentityRegisterService! + var storage: IdentityStorage! + var signer: CacaoMessageSigner! + + override func setUp() { + let keyserverURL = URL(string: "https://staging.keys.walletconnect.com")! + let httpService = HTTPNetworkClient(host: keyserverURL.host!) + let accountService = AccountService(currentAccount: account) + let identityNetworkService = IdentityNetworkService(accountService: accountService, httpService: httpService) + storage = IdentityStorage(keychain: KeychainStorageMock()) + sut = IdentityRegisterService( + keyserverURL: keyserverURL, + identityStorage: storage, + identityNetworkService: identityNetworkService, + iatProvader: DefaultIATProvider(), + messageFormatter: SIWECacaoFormatter() + ) + signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) + } + + func testRegisterIdentityAndInviteKey() async throws { + var message: String! + let publicKey = try await sut.registerIdentity(account: account, isPrivate: false) { msg in + message = msg + return try! signer.sign(message: msg, privateKey: privateKey, type: .eip191) + } + + let cacao = try await sut.resolveIdentity(publicKey: publicKey) + XCTAssertEqual(try SIWECacaoFormatter().formatMessage(from: cacao.p), message) + + let recovered = storage.getIdentityKey(for: account)!.publicKey.hexRepresentation + XCTAssertEqual(publicKey, recovered) + + let inviteKey = try await sut.registerInvite(account: account, isPrivate: false, onSign: { msg in + return try! signer.sign(message: msg, privateKey: privateKey, type: .eip191) + }) + + let recoveredKey = storage.getInviteKey(for: account)! + XCTAssertEqual(inviteKey, recoveredKey.publicKey.hexRepresentation) + + let resolvedKey = try await sut.resolveInvite(account: account) + XCTAssertEqual(inviteKey, resolvedKey) + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift index 361d0b8fe..90f554f02 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift @@ -8,8 +8,7 @@ final class AuthRequestInteractor { func approve(request: AuthRequest) async throws { let privateKey = Data(hex: "e56da0e170b5e09a8bb8f1b693392c7d56c3739a9c75740fbc558a2877868540") let signature = try signer.sign( - payload: request.payload, - address: account.address, + payload: request.payload.cacaoPayload(address: account.address), privateKey: privateKey, type: .eip191) try await Web3Wallet.instance.respond(requestId: request.id, signature: signature, from: account) diff --git a/Package.swift b/Package.swift index c633746d6..9a457fd6e 100644 --- a/Package.swift +++ b/Package.swift @@ -56,7 +56,7 @@ let package = Package( path: "Sources/Chat"), .target( name: "Auth", - dependencies: ["WalletConnectPairing"], + dependencies: ["WalletConnectPairing", "WalletConnectSigner"], path: "Sources/Auth"), .target( name: "Web3Wallet", @@ -72,7 +72,7 @@ let package = Package( path: "Sources/WalletConnectEcho"), .target( name: "WalletConnectRelay", - dependencies: ["WalletConnectKMS"], + dependencies: ["WalletConnectJWT"], path: "Sources/WalletConnectRelay", resources: [.copy("PackageConfig.json")]), .target( @@ -85,6 +85,12 @@ let package = Package( .target( name: "Web3Inbox", dependencies: ["WalletConnectChat"]), + .target( + name: "WalletConnectSigner", + dependencies: ["WalletConnectNetworking"]), + .target( + name: "WalletConnectJWT", + dependencies: ["WalletConnectKMS"]), .target( name: "WalletConnectUtils", dependencies: ["JSONRPC"]), diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 9ea611488..2ab5e242a 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -89,12 +89,12 @@ public class AuthClient: AuthClientProtocol { /// Query pending authentication requests /// - Returns: Pending authentication requests - public func getPendingRequests(account: Account) throws -> [AuthRequest] { - return try pendingRequestsProvider.getPendingRequests(account: account) + public func getPendingRequests() throws -> [AuthRequest] { + return try pendingRequestsProvider.getPendingRequests() } public func formatMessage(payload: AuthPayload, address: String) throws -> String { - return try SIWEMessageFormatter().formatMessage(from: payload, address: address) + return try SIWECacaoFormatter().formatMessage(from: payload.cacaoPayload(address: address)) } private func setUpPublishers() { diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 5fc6cb188..f9688eaf6 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -42,7 +42,7 @@ public struct AuthClientFactory { let kms = KeyManagementService(keychain: keychainStorage) let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) - let messageFormatter = SIWEMessageFormatter() + let messageFormatter = SIWECacaoFormatter() let appRequestService = AppRequestService(networkingInteractor: networkingClient, kms: kms, appMetadata: metadata, logger: logger, iatProvader: iatProvider) let messageSignerFactory = MessageSignerFactory(signerFactory: signerFactory) let messageSigner = messageSignerFactory.create(projectId: projectId) diff --git a/Sources/Auth/AuthClientProtocol.swift b/Sources/Auth/AuthClientProtocol.swift index 9e1a1fb62..31413cb76 100644 --- a/Sources/Auth/AuthClientProtocol.swift +++ b/Sources/Auth/AuthClientProtocol.swift @@ -7,5 +7,5 @@ public protocol AuthClientProtocol { func formatMessage(payload: AuthPayload, address: String) throws -> String func respond(requestId: RPCID, signature: CacaoSignature, from account: Account) async throws func reject(requestId: RPCID) async throws - func getPendingRequests(account: Account) throws -> [AuthRequest] + func getPendingRequests() throws -> [AuthRequest] } diff --git a/Sources/Auth/AuthImports.swift b/Sources/Auth/AuthImports.swift index 27245bda6..f27efa95c 100644 --- a/Sources/Auth/AuthImports.swift +++ b/Sources/Auth/AuthImports.swift @@ -1,3 +1,4 @@ #if !CocoaPods @_exported import WalletConnectPairing +@_exported import WalletConnectSigner #endif diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index d070880f5..70b48f95a 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -6,7 +6,7 @@ class AppRespondSubscriber { private let logger: ConsoleLogging private let rpcHistory: RPCHistory private let signatureVerifier: MessageSignatureVerifying - private let messageFormatter: SIWEMessageFormatting + private let messageFormatter: SIWECacaoFormatting private let pairingRegisterer: PairingRegisterer private var publishers = [AnyCancellable]() @@ -17,7 +17,7 @@ class AppRespondSubscriber { rpcHistory: RPCHistory, signatureVerifier: MessageSignatureVerifying, pairingRegisterer: PairingRegisterer, - messageFormatter: SIWEMessageFormatting) { + messageFormatter: SIWECacaoFormatting) { self.networkingInteractor = networkingInteractor self.logger = logger self.rpcHistory = rpcHistory @@ -52,8 +52,7 @@ class AppRespondSubscriber { guard let recovered = try? messageFormatter.formatMessage( - from: requestPayload.payloadParams, - address: address + from: requestPayload.payloadParams.cacaoPayload(address: address) ), recovered == message else { self.onResponse?(requestId, .failure(.messageCompromised)); return } diff --git a/Sources/Auth/Services/Common/IATProvider.swift b/Sources/Auth/Services/Common/IATProvider.swift deleted file mode 100644 index 1d386c11e..000000000 --- a/Sources/Auth/Services/Common/IATProvider.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -protocol IATProvider { - var iat: String { get } -} - -struct DefaultIATProvider: IATProvider { - var iat: String { - return ISO8601DateFormatter().string(from: Date()) - } -} diff --git a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift deleted file mode 100644 index 5e8c82d57..000000000 --- a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift +++ /dev/null @@ -1,109 +0,0 @@ -import Foundation - -protocol SIWEMessageFormatting { - func formatMessage(from payload: AuthPayload, address: String) throws -> String - func formatMessage(from payload: CacaoPayload) throws -> String -} - -public struct SIWEMessageFormatter: SIWEMessageFormatting { - - enum Errors: Error { - case invalidChainID - } - - public init() { } - - public func formatMessage(from payload: AuthPayload, address: String) throws -> String { - guard let chain = Blockchain(payload.chainId) else { - throw Errors.invalidChainID - } - let message = SIWEMessage( - domain: payload.domain, - uri: payload.aud, - address: address, - version: payload.version, - nonce: payload.nonce, - chainId: chain.reference, - iat: payload.iat, - nbf: payload.nbf, - exp: payload.exp, - statement: payload.statement, - requestId: payload.requestId, - resources: payload.resources - ) - return message.formatted - } - - func formatMessage(from payload: CacaoPayload) throws -> String { - let address = try DIDPKH(iss: payload.iss).account.address - let iss = try DIDPKH(iss: payload.iss) - let message = SIWEMessage( - domain: payload.domain, - uri: payload.aud, - address: address, - version: payload.version, - nonce: payload.nonce, - chainId: iss.account.reference, - iat: payload.iat, - nbf: payload.nbf, - exp: payload.exp, - statement: payload.statement, - requestId: payload.requestId, - resources: payload.resources - ) - return message.formatted - } -} - -private struct SIWEMessage: Equatable { - let domain: String - let uri: String // aud - let address: String - let version: String - let nonce: String - let chainId: String - let iat: String - let nbf: String? - let exp: String? - let statement: String? - let requestId: String? - let resources: [String]? - - var formatted: String { - return """ - \(domain) wants you to sign in with your Ethereum account: - \(address) - \(statementLine) - URI: \(uri) - Version: \(version) - Chain ID: \(chainId) - Nonce: \(nonce) - Issued At: \(iat)\(expLine)\(nbfLine)\(requestIdLine)\(resourcesSection) - """ - } - - var expLine: String { - guard let exp = exp else { return "" } - return "\nExpiration Time: \(exp)" - } - - var statementLine: String { - guard let statement = statement else { return "" } - return "\n\(statement)\n" - } - - var nbfLine: String { - guard let nbf = nbf else { return "" } - return "\nNot Before: \(nbf)" - } - - var requestIdLine: String { - guard let requestId = requestId else { return "" } - return "\nRequest ID: \(requestId)" - } - - var resourcesSection: String { - guard let resources = resources else { return "" } - return resources.reduce("\nResources:") { $0 + "\n- \($1)" } - } -} diff --git a/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift index 4aa8c04c0..524c029d5 100644 --- a/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift +++ b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift @@ -7,7 +7,7 @@ class PendingRequestsProvider { self.rpcHistory = rpcHistory } - public func getPendingRequests(account: Account) throws -> [AuthRequest] { + public func getPendingRequests() throws -> [AuthRequest] { let pendingRequests: [AuthRequest] = rpcHistory.getPending() .filter {$0.request.method == "wc_authRequest"} .compactMap { diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 595489aa3..62bb3427a 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -29,9 +29,8 @@ actor WalletRespondService { try kms.setAgreementSecret(keys, topic: topic) - let didpkh = DIDPKH(account: account) let header = CacaoHeader(t: "eip4361") - let payload = CacaoPayload(params: authRequestParams.payloadParams, didpkh: didpkh) + let payload = try authRequestParams.payloadParams.cacaoPayload(address: account.address) let responseParams = AuthResponseParams(h: header, p: payload, s: signature) let response = RPCResponse(id: requestId, result: responseParams) diff --git a/Sources/Auth/Types/AuthPayload.swift b/Sources/Auth/Types/AuthPayload.swift index 621df1ea3..0e6f7fd1d 100644 --- a/Sources/Auth/Types/AuthPayload.swift +++ b/Sources/Auth/Types/AuthPayload.swift @@ -28,4 +28,32 @@ public struct AuthPayload: Codable, Equatable { self.requestId = requestParams.requestId self.resources = requestParams.resources } + + public func cacaoPayload(address: String) throws -> CacaoPayload { + guard + let blockchain = Blockchain(chainId), + let account = Account(blockchain: blockchain, address: address) else { + throw Errors.invalidChainID + } + return CacaoPayload( + iss: DIDPKH(account: account).iss, + domain: domain, + aud: aud, + version: version, + nonce: nonce, + iat: iat, + nbf: nbf, + exp: exp, + statement: statement, + requestId: requestId, + resources: resources + ) + } +} + +private extension AuthPayload { + + enum Errors: Error { + case invalidChainID + } } diff --git a/Sources/Auth/Types/Cacao/Cacao.swift b/Sources/Auth/Types/Cacao/Cacao.swift deleted file mode 100644 index a82da2ab4..000000000 --- a/Sources/Auth/Types/Cacao/Cacao.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -/// CAIP-74 Cacao object -/// -/// specs at: https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-74.md -public struct Cacao: Codable, Equatable { - let h: CacaoHeader - let p: CacaoPayload - let s: CacaoSignature -} diff --git a/Sources/Auth/Types/Cacao/CacaoHeader.swift b/Sources/Auth/Types/Cacao/CacaoHeader.swift deleted file mode 100644 index 1461f3ae4..000000000 --- a/Sources/Auth/Types/Cacao/CacaoHeader.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -struct CacaoHeader: Codable, Equatable { - let t: String -} diff --git a/Sources/Auth/Types/Cacao/CacaoPayload.swift b/Sources/Auth/Types/Cacao/CacaoPayload.swift deleted file mode 100644 index 41f66e736..000000000 --- a/Sources/Auth/Types/Cacao/CacaoPayload.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation - -struct CacaoPayload: Codable, Equatable { - let iss: String - let domain: String - let aud: String - let version: String - let nonce: String - let iat: String - let nbf: String? - let exp: String? - let statement: String? - let requestId: String? - let resources: [String]? - - init(params: AuthPayload, didpkh: DIDPKH) { - self.iss = didpkh.iss - self.domain = params.domain - self.aud = params.aud - self.version = "1" - self.nonce = params.nonce - self.iat = params.iat - self.nbf = params.nbf - self.exp = params.exp - self.statement = params.statement - self.requestId = params.requestId - self.resources = params.resources - } -} diff --git a/Sources/Chat/ChatImports.swift b/Sources/Chat/ChatImports.swift index 23c1738ef..338babe27 100644 --- a/Sources/Chat/ChatImports.swift +++ b/Sources/Chat/ChatImports.swift @@ -1,3 +1,3 @@ #if !CocoaPods -@_exported import WalletConnectNetworking +@_exported import WalletConnectSigner #endif diff --git a/Sources/Chat/ProtocolServices/Identity/IdentityKey.swift b/Sources/Chat/ProtocolServices/Identity/IdentityKey.swift new file mode 100644 index 000000000..b172afbdb --- /dev/null +++ b/Sources/Chat/ProtocolServices/Identity/IdentityKey.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias IdentityKey = SigningPrivateKey diff --git a/Sources/Chat/ProtocolServices/Identity/IdentityKeyAPI.swift b/Sources/Chat/ProtocolServices/Identity/IdentityKeyAPI.swift new file mode 100644 index 000000000..1346d40ac --- /dev/null +++ b/Sources/Chat/ProtocolServices/Identity/IdentityKeyAPI.swift @@ -0,0 +1,64 @@ +import Foundation + +enum IdentityKeyAPI: HTTPService { + + case registerIdentity(cacao: Cacao) + case resolveIdentity(publicKey: String) + case removeIdentity(cacao: Cacao) + case registerInvite(idAuth: String) + case resolveInvite(account: String) + case removeInvite(idAuth: String) + + var path: String { + switch self { + case .registerIdentity, .resolveIdentity, .removeIdentity: + return "/identity" + case .registerInvite, .resolveInvite, .removeInvite: + return "/invite" + } + } + + var method: WalletConnectNetworking.HTTPMethod { + switch self { + case .registerIdentity, .registerInvite: + return .post + case .resolveIdentity, .resolveInvite: + return .get + case .removeInvite, .removeIdentity: + return .delete + } + } + + var body: Data? { + switch self { + case .registerIdentity(let cacao), .removeIdentity(let cacao): + return try? JSONEncoder().encode(RegisterIdentityRequest(cacao: cacao)) + case .registerInvite(let idAuth), .removeInvite(let idAuth): + return try? JSONEncoder().encode(RegisterInviteRequest(idAuth: idAuth)) + case .resolveIdentity, .resolveInvite: + return nil + } + } + + var queryParameters: [String : String]? { + switch self { + case .resolveIdentity(let publicKey): + return ["publicKey": publicKey] + case .resolveInvite(let account): + return ["account": account] + case .registerIdentity, .registerInvite, .removeInvite, .removeIdentity: + return nil + } + } +} + +private extension IdentityKeyAPI { + + struct RegisterIdentityRequest: Codable { + let cacao: Cacao + } + + struct RegisterInviteRequest: Codable { + let idAuth: String + } +} diff --git a/Sources/Chat/ProtocolServices/Identity/IdentityNetworkService.swift b/Sources/Chat/ProtocolServices/Identity/IdentityNetworkService.swift new file mode 100644 index 000000000..ca460fe7e --- /dev/null +++ b/Sources/Chat/ProtocolServices/Identity/IdentityNetworkService.swift @@ -0,0 +1,65 @@ +import Foundation + +actor IdentityNetworkService { + + private let accountService: AccountService + private let httpService: HTTPClient + + init(accountService: AccountService, httpService: HTTPClient) { + self.accountService = accountService + self.httpService = httpService + } + + // MARK: - IdentityKey + + func registerIdentity(cacao: Cacao) async throws { + let api = IdentityKeyAPI.registerIdentity(cacao: cacao) + try await httpService.request(service: api) + } + + func resolveIdentity(publicKey: String) async throws -> Cacao { + let api = IdentityKeyAPI.resolveIdentity(publicKey: publicKey) + let response = try await httpService.request(ResolveIdentityResponse.self, at: api) + return response.value.cacao + } + + func removeIdentity(cacao: Cacao) async throws { + let api = IdentityKeyAPI.removeIdentity(cacao: cacao) + try await httpService.request(service: api) + } + + // MARK: - InviteKey + + func registerInvite(idAuth: String) async throws { + let api = IdentityKeyAPI.registerInvite(idAuth: idAuth) + try await httpService.request(service: api) + } + + func resolveInvite(account: String) async throws -> String { + let api = IdentityKeyAPI.resolveInvite(account: account) + let response = try await httpService.request(ResolveInviteResponse.self, at: api) + return response.value.inviteKey + } + + func removeInvite(idAuth: String) async throws { + let api = IdentityKeyAPI.removeInvite(idAuth: idAuth) + try await httpService.request(service: api) + } +} + +private extension IdentityNetworkService { + + struct ResolveIdentityResponse: Codable { + struct Value: Codable { + let cacao: Cacao + } + let value: Value + } + + struct ResolveInviteResponse: Codable { + struct Value: Codable { + let inviteKey: String + } + let value: Value + } +} diff --git a/Sources/Chat/ProtocolServices/Identity/IdentityRegisterService.swift b/Sources/Chat/ProtocolServices/Identity/IdentityRegisterService.swift new file mode 100644 index 000000000..963290a9e --- /dev/null +++ b/Sources/Chat/ProtocolServices/Identity/IdentityRegisterService.swift @@ -0,0 +1,131 @@ +import Foundation + +actor IdentityRegisterService { + + private let keyserverURL: URL + private let identityStorage: IdentityStorage + private let identityNetworkService: IdentityNetworkService + private let iatProvader: IATProvider + private let messageFormatter: SIWECacaoFormatting + + init( + keyserverURL: URL, + identityStorage: IdentityStorage, + identityNetworkService: IdentityNetworkService, + iatProvader: IATProvider, + messageFormatter: SIWECacaoFormatting + ) { + self.keyserverURL = keyserverURL + self.identityStorage = identityStorage + self.identityNetworkService = identityNetworkService + self.iatProvader = iatProvader + self.messageFormatter = messageFormatter + } + + func registerIdentity(account: Account, + isPrivate: Bool, + onSign: (String) -> CacaoSignature + ) async throws -> String { + + if let identityKey = identityStorage.getIdentityKey(for: account) { + return identityKey.publicKey.hexRepresentation + } + + let identityKey = IdentityKey() + let cacao = try makeCacao(DIDKey: identityKey.DIDKey, account: account, onSign: onSign) + try await identityNetworkService.registerIdentity(cacao: cacao) + + // TODO: Handle private mode + + try identityStorage.saveIdentityKey(identityKey, for: account) + return identityKey.publicKey.hexRepresentation + } + + func registerInvite(account: Account, + isPrivate: Bool, + onSign: (String) -> CacaoSignature + ) async throws -> String { + + if let inviteKey = identityStorage.getInviteKey(for: account) { + return inviteKey.publicKey.hexRepresentation + } + + let inviteKey = IdentityKey() + let invitePublicKey = inviteKey.publicKey.hexRepresentation + let idAuth = try makeIDAuth(account: account, invitePublicKey: invitePublicKey) + try await identityNetworkService.registerInvite(idAuth: idAuth) + + // TODO: Handle private mode + + try identityStorage.saveInviteKey(inviteKey, for: account) + return inviteKey.publicKey.hexRepresentation + } + + func resolveIdentity(publicKey: String) async throws -> Cacao { + let data = Data(hex: publicKey) + let did = ED25519DIDKeyFactory().make(pubKey: data, prefix: false) + return try await identityNetworkService.resolveIdentity(publicKey: did) + } + + func resolveInvite(account: Account) async throws -> String { + return try await identityNetworkService.resolveInvite(account: account.absoluteString) + } +} + +private extension IdentityRegisterService { + + enum Errors: Error { + case identityKeyNotFound + } + + func makeCacao( + DIDKey: String, + account: Account, + onSign: (String) -> CacaoSignature + ) throws -> Cacao { + let cacaoHeader = CacaoHeader(t: "eip4361") + let cacaoPayload = CacaoPayload( + iss: account.iss, + domain: keyserverURL.host!, + aud: getAudience(), + version: getVersion(), + nonce: getNonce(), + iat: iatProvader.iat, + nbf: nil, exp: nil, statement: nil, requestId: nil, + resources: [DIDKey] + ) + let cacaoSignature = onSign(try messageFormatter.formatMessage(from: cacaoPayload)) + return Cacao(h: cacaoHeader, p: cacaoPayload, s: cacaoSignature) + } + + func makeIDAuth(account: Account, invitePublicKey: String) throws -> String { + guard let identityKey = identityStorage.getIdentityKey(for: account) + else { throw Errors.identityKeyNotFound } + return try JWTFactory().createAndSignJWT( + keyPair: identityKey, + sub: invitePublicKey, + aud: getAudience(), + exp: getExpiry(), + pkh: account.iss + ) + } + + private func getNonce() -> String { + return Data.randomBytes(count: 32).toHexString() + } + + private func getVersion() -> String { + return "1" + } + + private func getExpiry() -> Int { + var components = DateComponents() + components.setValue(1, for: .hour) + let date = Calendar.current.date(byAdding: components, to: Date())! + return Int(date.timeIntervalSince1970) + } + + private func getAudience() -> String { + return keyserverURL.absoluteString + } +} diff --git a/Sources/Chat/ProtocolServices/Identity/IdentityStorage.swift b/Sources/Chat/ProtocolServices/Identity/IdentityStorage.swift new file mode 100644 index 000000000..7f93bb075 --- /dev/null +++ b/Sources/Chat/ProtocolServices/Identity/IdentityStorage.swift @@ -0,0 +1,37 @@ +import Foundation + +final class IdentityStorage { + + private let keychain: KeychainStorageProtocol + + init(keychain: KeychainStorageProtocol) { + self.keychain = keychain + } + + func saveIdentityKey(_ key: IdentityKey, for account: Account) throws { + try keychain.add(key, forKey: identityKeyIdentifier(for: account)) + } + + func saveInviteKey(_ key: IdentityKey, for account: Account) throws { + try keychain.add(key, forKey: inviteKeyIdentifier(for: account)) + } + + func getIdentityKey(for account: Account) -> IdentityKey? { + return try? keychain.read(key: identityKeyIdentifier(for: account)) + } + + func getInviteKey(for account: Account) -> IdentityKey? { + return try? keychain.read(key: inviteKeyIdentifier(for: account)) + } +} + +private extension IdentityStorage { + + func identityKeyIdentifier(for account: Account) -> String { + return "com.walletconnect.chat.identity.\(account.absoluteString)" + } + + func inviteKeyIdentifier(for account: Account) -> String { + return "com.walletconnect.chat.invite.\(account.absoluteString)" + } +} diff --git a/Sources/WalletConnectRelay/ClientAuth/JWT/JWT+Claims.swift b/Sources/WalletConnectJWT/JWT+Claims.swift similarity index 95% rename from Sources/WalletConnectRelay/ClientAuth/JWT/JWT+Claims.swift rename to Sources/WalletConnectJWT/JWT+Claims.swift index 14ea4cffd..125a2b50a 100644 --- a/Sources/WalletConnectRelay/ClientAuth/JWT/JWT+Claims.swift +++ b/Sources/WalletConnectJWT/JWT+Claims.swift @@ -7,6 +7,7 @@ extension JWT { let aud: String let iat: Int let exp: Int + let pkh: String? func encode() throws -> String { let jsonEncoder = JSONEncoder() diff --git a/Sources/WalletConnectRelay/ClientAuth/JWT/JWT+Header.swift b/Sources/WalletConnectJWT/JWT+Header.swift similarity index 100% rename from Sources/WalletConnectRelay/ClientAuth/JWT/JWT+Header.swift rename to Sources/WalletConnectJWT/JWT+Header.swift diff --git a/Sources/WalletConnectRelay/ClientAuth/JWT/JWT.swift b/Sources/WalletConnectJWT/JWT.swift similarity index 85% rename from Sources/WalletConnectRelay/ClientAuth/JWT/JWT.swift rename to Sources/WalletConnectJWT/JWT.swift index 17663319b..7633fb991 100644 --- a/Sources/WalletConnectRelay/ClientAuth/JWT/JWT.swift +++ b/Sources/WalletConnectJWT/JWT.swift @@ -9,12 +9,12 @@ struct JWT: Codable, Equatable { var claims: Claims var signature: String? - public init(header: Header = Header(), claims: Claims) { + init(header: Header = Header(), claims: Claims) { self.header = header self.claims = claims } - public mutating func sign(using jwtSigner: JWTSigning) throws { + mutating func sign(using jwtSigner: JWTSigning) throws { header.alg = jwtSigner.alg let headerString = try header.encode() let claimsString = try claims.encode() diff --git a/Sources/WalletConnectRelay/ClientAuth/JWT/JWTEncoder.swift b/Sources/WalletConnectJWT/JWTEncoder.swift similarity index 100% rename from Sources/WalletConnectRelay/ClientAuth/JWT/JWTEncoder.swift rename to Sources/WalletConnectJWT/JWTEncoder.swift diff --git a/Sources/WalletConnectJWT/JWTFactory.swift b/Sources/WalletConnectJWT/JWTFactory.swift new file mode 100644 index 000000000..3b459561a --- /dev/null +++ b/Sources/WalletConnectJWT/JWTFactory.swift @@ -0,0 +1,21 @@ +import Foundation + +public struct JWTFactory { + + public init() { } + + public func createAndSignJWT( + keyPair: SigningPrivateKey, + sub: String, + aud: String, + exp: Int, + pkh: String? + ) throws -> String { + let now = Int(Date().timeIntervalSince1970) + let iss = keyPair.DIDKey + let claims = JWT.Claims(iss: iss, sub: sub, aud: aud, iat: now, exp: exp, pkh: pkh) + var jwt = JWT(claims: claims) + try jwt.sign(using: EdDSASigner(keyPair)) + return try jwt.encoded() + } +} diff --git a/Sources/WalletConnectJWT/JWTImports.swift b/Sources/WalletConnectJWT/JWTImports.swift new file mode 100644 index 000000000..24d4a8cab --- /dev/null +++ b/Sources/WalletConnectJWT/JWTImports.swift @@ -0,0 +1,3 @@ +#if !CocoaPods +@_exported import WalletConnectKMS +#endif diff --git a/Sources/WalletConnectRelay/ClientAuth/JWT/JWTSigning.swift b/Sources/WalletConnectJWT/JWTSigning.swift similarity index 100% rename from Sources/WalletConnectRelay/ClientAuth/JWT/JWTSigning.swift rename to Sources/WalletConnectJWT/JWTSigning.swift diff --git a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/SigningKeyCryptoKit.swift b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/SigningKeyCryptoKit.swift index f02cc72cb..527463fed 100644 --- a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/SigningKeyCryptoKit.swift +++ b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/SigningKeyCryptoKit.swift @@ -79,4 +79,10 @@ public struct SigningPrivateKey: GenericPasswordConvertible, Equatable { public func signature(_ data: Data) throws -> Data { return try key.signature(for: data) } + + public var DIDKey: String { + return ED25519DIDKeyFactory().make( + pubKey: publicKey.rawRepresentation, prefix: true + ) + } } diff --git a/Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift b/Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift index 6858dcad5..1fbf12bac 100644 --- a/Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift +++ b/Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift @@ -17,25 +17,20 @@ struct SocketAuthenticator: SocketAuthenticating { func createAuthToken() throws -> String { let clientIdKeyPair = try clientIdStorage.getOrCreateKeyPair() - let subject = generateSubject() - return try createAndSignJWT(subject: subject, keyPair: clientIdKeyPair) + return try createAndSignJWT(keyPair: clientIdKeyPair) } - private func createAndSignJWT(subject: String, keyPair: SigningPrivateKey) throws -> String { - let issuer = didKeyFactory.make(pubKey: keyPair.publicKey.rawRepresentation, prefix: true) - let now = Int(Date().timeIntervalSince1970) - let claims = JWT.Claims(iss: issuer, sub: subject, aud: getAudience(), iat: now, exp: getExpiry()) - var jwt = JWT(claims: claims) - try jwt.sign(using: EdDSASigner(keyPair)) - return try jwt.encoded() - } - - private func generateSubject() -> String { - return Data.randomBytes(count: 32).toHexString() + private func createAndSignJWT(keyPair: SigningPrivateKey) throws -> String { + return try JWTFactory().createAndSignJWT( + keyPair: keyPair, + sub: getSubject(), + aud: getAudience(), + exp: getExpiry(), + pkh: nil + ) } private func getExpiry() -> Int { - var components = DateComponents() components.setValue(1, for: .day) // safe to unwrap as the date must be calculated @@ -46,4 +41,8 @@ struct SocketAuthenticator: SocketAuthenticating { private func getAudience() -> String { return "wss://\(relayHost)" } + + private func getSubject() -> String { + return Data.randomBytes(count: 32).toHexString() + } } diff --git a/Sources/WalletConnectRelay/RelayImports.swift b/Sources/WalletConnectRelay/RelayImports.swift index 24d4a8cab..5c5e68a01 100644 --- a/Sources/WalletConnectRelay/RelayImports.swift +++ b/Sources/WalletConnectRelay/RelayImports.swift @@ -1,3 +1,3 @@ #if !CocoaPods -@_exported import WalletConnectKMS +@_exported import WalletConnectJWT #endif diff --git a/Sources/Auth/Services/Signer/Ethereum/EIP1271/EIP1271Verifier.swift b/Sources/WalletConnectSigner/Signer/Ethereum/EIP1271/EIP1271Verifier.swift similarity index 100% rename from Sources/Auth/Services/Signer/Ethereum/EIP1271/EIP1271Verifier.swift rename to Sources/WalletConnectSigner/Signer/Ethereum/EIP1271/EIP1271Verifier.swift diff --git a/Sources/Auth/Services/Signer/Ethereum/EIP1271/RPCService.swift b/Sources/WalletConnectSigner/Signer/Ethereum/EIP1271/RPCService.swift similarity index 100% rename from Sources/Auth/Services/Signer/Ethereum/EIP1271/RPCService.swift rename to Sources/WalletConnectSigner/Signer/Ethereum/EIP1271/RPCService.swift diff --git a/Sources/Auth/Services/Signer/Ethereum/EIP1271/ValidSignatureMethod.swift b/Sources/WalletConnectSigner/Signer/Ethereum/EIP1271/ValidSignatureMethod.swift similarity index 100% rename from Sources/Auth/Services/Signer/Ethereum/EIP1271/ValidSignatureMethod.swift rename to Sources/WalletConnectSigner/Signer/Ethereum/EIP1271/ValidSignatureMethod.swift diff --git a/Sources/Auth/Services/Signer/Ethereum/EIP191/EIP191Verifier.swift b/Sources/WalletConnectSigner/Signer/Ethereum/EIP191/EIP191Verifier.swift similarity index 100% rename from Sources/Auth/Services/Signer/Ethereum/EIP191/EIP191Verifier.swift rename to Sources/WalletConnectSigner/Signer/Ethereum/EIP191/EIP191Verifier.swift diff --git a/Sources/Auth/Services/Signer/Ethereum/EthereumSignature.swift b/Sources/WalletConnectSigner/Signer/EthereumSignature.swift similarity index 100% rename from Sources/Auth/Services/Signer/Ethereum/EthereumSignature.swift rename to Sources/WalletConnectSigner/Signer/EthereumSignature.swift diff --git a/Sources/Auth/Services/Signer/Ethereum/EthereumSigner.swift b/Sources/WalletConnectSigner/Signer/EthereumSigner.swift similarity index 100% rename from Sources/Auth/Services/Signer/Ethereum/EthereumSigner.swift rename to Sources/WalletConnectSigner/Signer/EthereumSigner.swift diff --git a/Sources/Auth/Services/Signer/MessageSigner.swift b/Sources/WalletConnectSigner/Signer/MessageSigner.swift similarity index 76% rename from Sources/Auth/Services/Signer/MessageSigner.swift rename to Sources/WalletConnectSigner/Signer/MessageSigner.swift index 7af8792a2..b4eccf811 100644 --- a/Sources/Auth/Services/Signer/MessageSigner.swift +++ b/Sources/WalletConnectSigner/Signer/MessageSigner.swift @@ -9,16 +9,20 @@ public protocol MessageSignatureVerifying { } public protocol MessageSigning { - func sign(payload: AuthPayload, - address: String, + func sign(message: String, + privateKey: Data, + type: CacaoSignatureType + ) throws -> CacaoSignature + + func sign(payload: CacaoPayload, privateKey: Data, type: CacaoSignatureType ) throws -> CacaoSignature } -public typealias AuthMessageSigner = MessageSignatureVerifying & MessageSigning +public typealias CacaoMessageSigner = MessageSignatureVerifying & MessageSigning -struct MessageSigner: AuthMessageSigner { +struct MessageSigner: CacaoMessageSigner { enum Errors: Error { case utf8EncodingFailed @@ -27,22 +31,28 @@ struct MessageSigner: AuthMessageSigner { private let signer: EthereumSigner private let eip191Verifier: EIP191Verifier private let eip1271Verifier: EIP1271Verifier - private let messageFormatter: SIWEMessageFormatting + private let messageFormatter: SIWECacaoFormatting - init(signer: EthereumSigner, eip191Verifier: EIP191Verifier, eip1271Verifier: EIP1271Verifier, messageFormatter: SIWEMessageFormatting) { + init(signer: EthereumSigner, eip191Verifier: EIP191Verifier, eip1271Verifier: EIP1271Verifier, messageFormatter: SIWECacaoFormatting) { self.signer = signer self.eip191Verifier = eip191Verifier self.eip1271Verifier = eip1271Verifier self.messageFormatter = messageFormatter } - func sign(payload: AuthPayload, - address: String, + func sign(payload: CacaoPayload, privateKey: Data, type: CacaoSignatureType ) throws -> CacaoSignature { - let message = try messageFormatter.formatMessage(from: payload, address: address) + let message = try messageFormatter.formatMessage(from: payload) + return try sign(message: message, privateKey: privateKey, type: type) + } + + func sign(message: String, + privateKey: Data, + type: CacaoSignatureType + ) throws -> CacaoSignature { guard let messageData = message.data(using: .utf8)else { throw Errors.utf8EncodingFailed diff --git a/Sources/Auth/Services/Signer/MessageSignerFactory.swift b/Sources/WalletConnectSigner/Signer/MessageSignerFactory.swift similarity index 80% rename from Sources/Auth/Services/Signer/MessageSignerFactory.swift rename to Sources/WalletConnectSigner/Signer/MessageSignerFactory.swift index 88d56d374..ef4f53879 100644 --- a/Sources/Auth/Services/Signer/MessageSignerFactory.swift +++ b/Sources/WalletConnectSigner/Signer/MessageSignerFactory.swift @@ -8,11 +8,11 @@ public struct MessageSignerFactory { self.signerFactory = signerFactory } - public func create() -> AuthMessageSigner { + public func create() -> CacaoMessageSigner { return create(projectId: Networking.projectId) } - func create(projectId: String) -> AuthMessageSigner { + public func create(projectId: String) -> CacaoMessageSigner { return MessageSigner( signer: signerFactory.createEthereumSigner(), eip191Verifier: EIP191Verifier(signer: signerFactory.createEthereumSigner()), @@ -21,7 +21,7 @@ public struct MessageSignerFactory { httpClient: HTTPNetworkClient(host: "rpc.walletconnect.com"), signer: signerFactory.createEthereumSigner() ), - messageFormatter: SIWEMessageFormatter() + messageFormatter: SIWECacaoFormatter() ) } } diff --git a/Sources/Auth/Services/Signer/SignerFactory.swift b/Sources/WalletConnectSigner/Signer/SignerFactory.swift similarity index 100% rename from Sources/Auth/Services/Signer/SignerFactory.swift rename to Sources/WalletConnectSigner/Signer/SignerFactory.swift diff --git a/Sources/WalletConnectSigner/SignerImports.swift b/Sources/WalletConnectSigner/SignerImports.swift new file mode 100644 index 000000000..23c1738ef --- /dev/null +++ b/Sources/WalletConnectSigner/SignerImports.swift @@ -0,0 +1,3 @@ +#if !CocoaPods +@_exported import WalletConnectNetworking +#endif diff --git a/Sources/WalletConnectUtils/Cacao/Cacao.swift b/Sources/WalletConnectUtils/Cacao/Cacao.swift new file mode 100644 index 000000000..5c938594b --- /dev/null +++ b/Sources/WalletConnectUtils/Cacao/Cacao.swift @@ -0,0 +1,16 @@ +import Foundation + +/// CAIP-74 Cacao object +/// +/// specs at: https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-74.md +public struct Cacao: Codable, Equatable { + public let h: CacaoHeader + public let p: CacaoPayload + public let s: CacaoSignature + + public init(h: CacaoHeader, p: CacaoPayload, s: CacaoSignature) { + self.h = h + self.p = p + self.s = s + } +} diff --git a/Sources/WalletConnectUtils/Cacao/CacaoHeader.swift b/Sources/WalletConnectUtils/Cacao/CacaoHeader.swift new file mode 100644 index 000000000..897c314d4 --- /dev/null +++ b/Sources/WalletConnectUtils/Cacao/CacaoHeader.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct CacaoHeader: Codable, Equatable { + public let t: String + + public init(t: String) { + self.t = t + } +} diff --git a/Sources/WalletConnectUtils/Cacao/CacaoPayload.swift b/Sources/WalletConnectUtils/Cacao/CacaoPayload.swift new file mode 100644 index 000000000..3711f3ef0 --- /dev/null +++ b/Sources/WalletConnectUtils/Cacao/CacaoPayload.swift @@ -0,0 +1,41 @@ +import Foundation + +public struct CacaoPayload: Codable, Equatable { + public let iss: String + public let domain: String + public let aud: String + public let version: String + public let nonce: String + public let iat: String + public let nbf: String? + public let exp: String? + public let statement: String? + public let requestId: String? + public let resources: [String]? + + public init( + iss: String, + domain: String, + aud: String, + version: String, + nonce: String, + iat: String, + nbf: String?, + exp: String?, + statement: String?, + requestId: String?, + resources: [String]? + ) { + self.iss = iss + self.domain = domain + self.aud = aud + self.version = version + self.nonce = nonce + self.iat = iat + self.nbf = nbf + self.exp = exp + self.statement = statement + self.requestId = requestId + self.resources = resources + } +} diff --git a/Sources/Auth/Types/Cacao/CacaoSignature.swift b/Sources/WalletConnectUtils/Cacao/CacaoSignature.swift similarity index 76% rename from Sources/Auth/Types/Cacao/CacaoSignature.swift rename to Sources/WalletConnectUtils/Cacao/CacaoSignature.swift index 7137f9dea..d272f0279 100644 --- a/Sources/Auth/Types/Cacao/CacaoSignature.swift +++ b/Sources/WalletConnectUtils/Cacao/CacaoSignature.swift @@ -6,9 +6,9 @@ public enum CacaoSignatureType: String, Codable { } public struct CacaoSignature: Codable, Equatable { - let t: CacaoSignatureType - let s: String - let m: String? + public let t: CacaoSignatureType + public let s: String + public let m: String? public init(t: CacaoSignatureType, s: String, m: String? = nil) { self.t = t diff --git a/Sources/WalletConnectUtils/Cacao/IATProvider.swift b/Sources/WalletConnectUtils/Cacao/IATProvider.swift new file mode 100644 index 000000000..22aefa45d --- /dev/null +++ b/Sources/WalletConnectUtils/Cacao/IATProvider.swift @@ -0,0 +1,14 @@ +import Foundation + +public protocol IATProvider { + var iat: String { get } +} + +public struct DefaultIATProvider: IATProvider { + + public init() { } + + public var iat: String { + return ISO8601DateFormatter().string(from: Date()) + } +} diff --git a/Sources/WalletConnectRelay/ClientAuth/Base58.swift b/Sources/WalletConnectUtils/DID/Base58.swift similarity index 100% rename from Sources/WalletConnectRelay/ClientAuth/Base58.swift rename to Sources/WalletConnectUtils/DID/Base58.swift diff --git a/Sources/WalletConnectRelay/ClientAuth/DIDKeyFactory.swift b/Sources/WalletConnectUtils/DID/DIDKeyFactory.swift similarity index 83% rename from Sources/WalletConnectRelay/ClientAuth/DIDKeyFactory.swift rename to Sources/WalletConnectUtils/DID/DIDKeyFactory.swift index 2a062c090..1d3bb7066 100644 --- a/Sources/WalletConnectRelay/ClientAuth/DIDKeyFactory.swift +++ b/Sources/WalletConnectUtils/DID/DIDKeyFactory.swift @@ -1,19 +1,21 @@ import Foundation -protocol DIDKeyFactory { +public protocol DIDKeyFactory { func make(pubKey: Data, prefix: Bool) -> String } /// A DID Method for Static Cryptographic Keys /// did-key-format := did:key:MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes)) -struct ED25519DIDKeyFactory: DIDKeyFactory { +public struct ED25519DIDKeyFactory: DIDKeyFactory { private let DID_DELIMITER = ":" private let DID_PREFIX = "did" private let DID_METHOD = "key" private let MULTICODEC_ED25519_HEADER: [UInt8] = [0xed, 0x01] private let MULTICODEC_ED25519_BASE = "z" - func make(pubKey: Data, prefix: Bool) -> String { + public init() { } + + public func make(pubKey: Data, prefix: Bool) -> String { let multibase = multibase(pubKey: pubKey) guard prefix else { return multibase } diff --git a/Sources/Auth/Services/Signer/DIDPKH.swift b/Sources/WalletConnectUtils/DID/DIDPKH.swift similarity index 65% rename from Sources/Auth/Services/Signer/DIDPKH.swift rename to Sources/WalletConnectUtils/DID/DIDPKH.swift index 1475a6530..065569cc2 100644 --- a/Sources/Auth/Services/Signer/DIDPKH.swift +++ b/Sources/WalletConnectUtils/DID/DIDPKH.swift @@ -1,17 +1,18 @@ import Foundation -struct DIDPKH { - static let didPrefix: String = "did:pkh" +public struct DIDPKH { + + private static let didPrefix: String = "did:pkh" enum Errors: Error { case invalidDIDPKH case invalidAccount } - let account: Account - let iss: String + public let account: Account + public let iss: String - init(iss: String) throws { + public init(iss: String) throws { guard iss.starts(with: DIDPKH.didPrefix) else { throw Errors.invalidDIDPKH } @@ -25,8 +26,15 @@ struct DIDPKH { self.account = account } - init(account: Account) { + public init(account: Account) { self.iss = "\(DIDPKH.didPrefix):\(account.absoluteString)" self.account = account } } + +extension Account { + + public var iss: String { + return DIDPKH(account: self).iss + } +} diff --git a/Sources/WalletConnectUtils/SIWE/SIWECacaoFormatter.swift b/Sources/WalletConnectUtils/SIWE/SIWECacaoFormatter.swift new file mode 100644 index 000000000..c361677a3 --- /dev/null +++ b/Sources/WalletConnectUtils/SIWE/SIWECacaoFormatter.swift @@ -0,0 +1,30 @@ +import Foundation + +public protocol SIWECacaoFormatting { + func formatMessage(from payload: CacaoPayload) throws -> String +} + +public struct SIWECacaoFormatter: SIWECacaoFormatting { + + public init() { } + + public func formatMessage(from payload: CacaoPayload) throws -> String { + let address = try DIDPKH(iss: payload.iss).account.address + let iss = try DIDPKH(iss: payload.iss) + let message = SIWEMessage( + domain: payload.domain, + uri: payload.aud, + address: address, + version: payload.version, + nonce: payload.nonce, + chainId: iss.account.reference, + iat: payload.iat, + nbf: payload.nbf, + exp: payload.exp, + statement: payload.statement, + requestId: payload.requestId, + resources: payload.resources + ) + return message.formatted + } +} diff --git a/Sources/WalletConnectUtils/SIWE/SIWEMessage.swift b/Sources/WalletConnectUtils/SIWE/SIWEMessage.swift new file mode 100644 index 000000000..0a7fc18df --- /dev/null +++ b/Sources/WalletConnectUtils/SIWE/SIWEMessage.swift @@ -0,0 +1,72 @@ +import Foundation + +public struct SIWEMessage: Equatable { + public let domain: String + public let uri: String // aud + public let address: String + public let version: String + public let nonce: String + public let chainId: String + public let iat: String + public let nbf: String? + public let exp: String? + public let statement: String? + public let requestId: String? + public let resources: [String]? + + public init(domain: String, uri: String, address: String, version: String, nonce: String, chainId: String, iat: String, nbf: String?, exp: String?, statement: String?, requestId: String?, resources: [String]?) { + self.domain = domain + self.uri = uri + self.address = address + self.version = version + self.nonce = nonce + self.chainId = chainId + self.iat = iat + self.nbf = nbf + self.exp = exp + self.statement = statement + self.requestId = requestId + self.resources = resources + } + + public var formatted: String { + return """ + \(domain) wants you to sign in with your Ethereum account: + \(address) + \(statementLine) + URI: \(uri) + Version: \(version) + Chain ID: \(chainId) + Nonce: \(nonce) + Issued At: \(iat)\(expLine)\(nbfLine)\(requestIdLine)\(resourcesSection) + """ + } +} + +private extension SIWEMessage { + + var expLine: String { + guard let exp = exp else { return "" } + return "\nExpiration Time: \(exp)" + } + + var statementLine: String { + guard let statement = statement else { return "" } + return "\n\(statement)\n" + } + + var nbfLine: String { + guard let nbf = nbf else { return "" } + return "\nNot Before: \(nbf)" + } + + var requestIdLine: String { + guard let requestId = requestId else { return "" } + return "\nRequest ID: \(requestId)" + } + + var resourcesSection: String { + guard let resources = resources else { return "" } + return resources.reduce("\nResources:") { $0 + "\n- \($1)" } + } +} diff --git a/Sources/Web3Wallet/Web3WalletClient.swift b/Sources/Web3Wallet/Web3WalletClient.swift index 43c709b95..d9397de4e 100644 --- a/Sources/Web3Wallet/Web3WalletClient.swift +++ b/Sources/Web3Wallet/Web3WalletClient.swift @@ -169,7 +169,7 @@ public class Web3WalletClient { /// Query pending authentication requests /// - Returns: Pending authentication requests - public func getPendingRequests(account: Account) throws -> [AuthRequest] { - try authClient.getPendingRequests(account: account) + public func getPendingRequests() throws -> [AuthRequest] { + try authClient.getPendingRequests() } } diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index 72f3f4f51..473ea5d1d 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -11,16 +11,16 @@ class AppRespondSubscriberTests: XCTestCase { var networkingInteractor: NetworkingInteractorMock! var sut: AppRespondSubscriber! - var messageFormatter: SIWEMessageFormatter! + var messageFormatter: SIWECacaoFormatter! var rpcHistory: RPCHistory! let defaultTimeout: TimeInterval = 0.01 - var messageSigner: AuthMessageSigner! + var messageSigner: CacaoMessageSigner! var pairingStorage: WCPairingStorageMock! var pairingRegisterer: PairingRegistererMock! override func setUp() { networkingInteractor = NetworkingInteractorMock() - messageFormatter = SIWEMessageFormatter() + messageFormatter = SIWECacaoFormatter() messageSigner = MessageSignerMock() rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: RuntimeKeyValueStorage()) pairingStorage = WCPairingStorageMock() @@ -58,9 +58,8 @@ class AppRespondSubscriberTests: XCTestCase { } // subscribe on compromised cacao - let account = Account(chainIdentifier: "eip155:1", address: "0x724d0D2DaD3fbB0C168f947B87Fa5DBe36F1A8bf")! let cacaoHeader = CacaoHeader(t: "eip4361") - let cacaoPayload = CacaoPayload(params: compromissedParams.payloadParams, didpkh: DIDPKH(account: account)) + let cacaoPayload = try! compromissedParams.payloadParams.cacaoPayload(address: "0x724d0D2DaD3fbB0C168f947B87Fa5DBe36F1A8bf") let cacaoSignature = CacaoSignature(t: .eip191, s: "") let cacao = Cacao(h: cacaoHeader, p: cacaoPayload, s: cacaoSignature) diff --git a/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift b/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift index 64adc7c96..0796eb6ce 100644 --- a/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift +++ b/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift @@ -1,13 +1,9 @@ import Foundation @testable import Auth -class SIWEMessageFormatterMock: SIWEMessageFormatting { +class SIWEMessageFormatterMock: SIWECacaoFormatting { var formattedMessage: String! - func formatMessage(from authPayload: AuthPayload, address: String) throws -> String { - return formattedMessage - } - func formatMessage(from payload: CacaoPayload) throws -> String { return formattedMessage } diff --git a/Tests/AuthTests/SIWEMessageFormatterTests.swift b/Tests/AuthTests/SIWEMessageFormatterTests.swift index 859b2cf41..129fe4c52 100644 --- a/Tests/AuthTests/SIWEMessageFormatterTests.swift +++ b/Tests/AuthTests/SIWEMessageFormatterTests.swift @@ -3,11 +3,11 @@ import Foundation import XCTest class SIWEMessageFormatterTests: XCTestCase { - var sut: SIWEMessageFormatter! + var sut: SIWECacaoFormatter! let address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" override func setUp() { - sut = SIWEMessageFormatter() + sut = SIWECacaoFormatter() } func testFormatMessage() throws { @@ -27,7 +27,7 @@ class SIWEMessageFormatterTests: XCTestCase { - ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/ - https://example.com/my-web2-claim.json """ - let message = try sut.formatMessage(from: AuthPayload.stub(), address: address) + let message = try sut.formatMessage(from: AuthPayload.stub().cacaoPayload(address: address)) XCTAssertEqual(message, expectedMessage) } @@ -48,8 +48,9 @@ class SIWEMessageFormatterTests: XCTestCase { """ let message = try sut.formatMessage( from: AuthPayload.stub( - requestParams: RequestParams.stub(statement: nil)), - address: address) + requestParams: RequestParams.stub(statement: nil) + ).cacaoPayload(address: address) + ) XCTAssertEqual(message, expectedMessage) } @@ -69,8 +70,8 @@ class SIWEMessageFormatterTests: XCTestCase { """ let message = try sut.formatMessage( from: AuthPayload.stub( - requestParams: RequestParams.stub(resources: nil)), - address: address) + requestParams: RequestParams.stub(resources: nil)).cacaoPayload(address: address) + ) XCTAssertEqual(message, expectedMessage) } @@ -88,9 +89,8 @@ class SIWEMessageFormatterTests: XCTestCase { """ let message = try sut.formatMessage( from: AuthPayload.stub( - requestParams: RequestParams.stub(statement: nil, - resources: nil)), - address: address) + requestParams: RequestParams.stub(statement: nil, resources: nil)).cacaoPayload(address: address) + ) XCTAssertEqual(message, expectedMessage) } } diff --git a/Tests/AuthTests/Stubs/MessageSignerMock.swift b/Tests/AuthTests/Stubs/MessageSignerMock.swift index 4275cd1b9..869febeeb 100644 --- a/Tests/AuthTests/Stubs/MessageSignerMock.swift +++ b/Tests/AuthTests/Stubs/MessageSignerMock.swift @@ -1,7 +1,10 @@ import Foundation import Auth -struct MessageSignerMock: AuthMessageSigner { +struct MessageSignerMock: CacaoMessageSigner { + func sign(message: String, privateKey: Data, type: WalletConnectUtils.CacaoSignatureType) throws -> WalletConnectUtils.CacaoSignature { + return CacaoSignature(t: .eip191, s: "") + } func verify(signature: CacaoSignature, message: String, @@ -11,8 +14,7 @@ struct MessageSignerMock: AuthMessageSigner { } - func sign(payload: AuthPayload, - address: String, + func sign(payload: CacaoPayload, privateKey: Data, type: CacaoSignatureType ) throws -> CacaoSignature { diff --git a/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift b/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift index 2e5643785..913a2bed1 100644 --- a/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift +++ b/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift @@ -2,6 +2,7 @@ import Foundation import XCTest import WalletConnectKMS @testable import WalletConnectRelay +@testable import WalletConnectJWT final class EdDSASignerTests: XCTestCase { var sut: EdDSASigner! diff --git a/Tests/RelayerTests/AuthTests/JWTTests.swift b/Tests/RelayerTests/AuthTests/JWTTests.swift index 8f20cd961..d2eb4d8b6 100644 --- a/Tests/RelayerTests/AuthTests/JWTTests.swift +++ b/Tests/RelayerTests/AuthTests/JWTTests.swift @@ -1,6 +1,7 @@ import Foundation import XCTest @testable import WalletConnectRelay +@testable import WalletConnectJWT final class JWTTests: XCTestCase { let expectedJWT = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTY5MTAwOTcsImV4cCI6MTY1Njk5NjQ5NywiaXNzIjoiZGlkOmtleTp6Nk1rb2RIWnduZVZSU2h0YUxmOEpLWWt4cERHcDF2R1pucEdtZEJwWDhNMmV4eEgiLCJzdWIiOiJjNDc5ZmU1ZGM0NjRlNzcxZTc4YjE5M2QyMzlhNjViNThkMjc4Y2FkMWMzNGJmYjBiNTcxNmU1YmI1MTQ5MjhlIiwiYXVkIjoid3NzOi8vcmVsYXkud2FsbGV0Y29ubmVjdC5jb20ifQ.0JkxOM-FV21U7Hk-xycargj_qNRaYV2H5HYtE4GzAeVQYiKWj7YySY5AdSqtCgGzX4Gt98XWXn2kSr9rE1qvCA" @@ -27,6 +28,6 @@ extension JWT.Claims { let aud = "wss://relay.walletconnect.com" let expDate = Calendar.current.date(byAdding: components, to: iatDate)! let exp = Int(expDate.timeIntervalSince1970) - return JWT.Claims(iss: iss, sub: sub, aud: aud, iat: iat, exp: exp) + return JWT.Claims(iss: iss, sub: sub, aud: aud, iat: iat, exp: exp, pkh: nil) } } diff --git a/Tests/RelayerTests/Mocks/EdDSASignerMock.swift b/Tests/RelayerTests/Mocks/EdDSASignerMock.swift index b7ca50718..dc52ed565 100644 --- a/Tests/RelayerTests/Mocks/EdDSASignerMock.swift +++ b/Tests/RelayerTests/Mocks/EdDSASignerMock.swift @@ -1,4 +1,5 @@ import Foundation +@testable import WalletConnectJWT @testable import WalletConnectRelay class EdDSASignerMock: JWTSigning { diff --git a/Tests/Web3WalletTests/Mocks/AuthClientMock.swift b/Tests/Web3WalletTests/Mocks/AuthClientMock.swift index 3b1a3a68d..4f3e50c39 100644 --- a/Tests/Web3WalletTests/Mocks/AuthClientMock.swift +++ b/Tests/Web3WalletTests/Mocks/AuthClientMock.swift @@ -42,7 +42,7 @@ final class AuthClientMock: AuthClientProtocol { rejectCalled = true } - func getPendingRequests(account: WalletConnectUtils.Account) throws -> [AuthRequest] { + func getPendingRequests() throws -> [AuthRequest] { return [authRequest] } } diff --git a/Tests/Web3WalletTests/Web3WalletTests.swift b/Tests/Web3WalletTests/Web3WalletTests.swift index 7ce173cc5..8962e88a1 100644 --- a/Tests/Web3WalletTests/Web3WalletTests.swift +++ b/Tests/Web3WalletTests/Web3WalletTests.swift @@ -186,8 +186,7 @@ final class Web3WalletTests: XCTestCase { } func testAuthPendingRequestsCalledAndNotEmpty() async { - let account = Account("eip155:56:0xe5EeF1368781911d265fDB6946613dA61915a501")! - let pendingRequests = try! web3WalletClient.getPendingRequests(account: account) + let pendingRequests = try! web3WalletClient.getPendingRequests() XCTAssertEqual(1, pendingRequests.count) } }