Skip to content

Commit

Permalink
Merge pull request #1200 from WalletConnect/feature/dapp-pub-key-for-…
Browse files Browse the repository at this point in the history
…subscription

[Notify] AppPubKey for subscription /  subscriptionChange subscriber fixed
  • Loading branch information
flypaper0 authored Oct 29, 2023
2 parents fdf6517 + 7fe66a2 commit f834829
Show file tree
Hide file tree
Showing 18 changed files with 131 additions and 145 deletions.
12 changes: 7 additions & 5 deletions Example/IntegrationTests/Push/NotifyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,20 +136,22 @@ final class NotifyTests: XCTestCase {
let expectation = expectation(description: "expects client B to receive subscription after both clients are registered and client A creates one")
expectation.assertForOverFulfill = false

var subscription: NotifySubscription!

let clientB = makeWalletClient(prefix: "👐🏼 Wallet B: ")
clientB.subscriptionsPublisher.sink { subscriptions in
guard let subscription = subscriptions.first else { return }
Task(priority: .high) {
try await clientB.deleteSubscription(topic: subscription.topic)
expectation.fulfill()
}
guard let newSubscription = subscriptions.first else { return }
subscription = newSubscription
expectation.fulfill()
}.store(in: &publishers)

try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign)
try! await clientB.register(account: account, domain: gmDappDomain, onSign: sign)
try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account)

wait(for: [expectation], timeout: InputConfig.defaultTimeout)

try await clientB.deleteSubscription(topic: subscription.topic)
}

func testWalletCreatesAndUpdatesSubscription() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,12 @@ final class NotificationsInteractor {
return try await withCheckedThrowingContinuation { continuation in
var cancellable: AnyCancellable?
cancellable = subscriptionsPublisher
.setFailureType(to: Error.self)
.timeout(10, scheduler: RunLoop.main, customError: { Errors.subscribeTimeout })
.sink(receiveCompletion: { completion in
defer { cancellable?.cancel() }
switch completion {
case .failure(let error): continuation.resume(with: .failure(error))
case .finished: break
}
}, receiveValue: { subscriptions in
.sink { subscriptions in
guard subscriptions.contains(where: { $0.metadata.url == domain }) else { return }
cancellable?.cancel()
continuation.resume(with: .success(()))
})

}
Task { [cancellable] in
do {
try await Notify.instance.subscribe(appDomain: domain, account: importAccount.account)
Expand Down
7 changes: 6 additions & 1 deletion Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public class NotifyClient {
public func register(account: Account, domain: String, isLimited: Bool = false, onSign: @escaping SigningCallback) async throws {
try await identityService.register(account: account, domain: domain, isLimited: isLimited, onSign: onSign)
notifyAccountProvider.setAccount(account)
subscriptionWatcher.start()
try await subscriptionWatcher.start()
}

public func unregister(account: Account) async throws {
Expand Down Expand Up @@ -132,6 +132,11 @@ public class NotifyClient {

#if targetEnvironment(simulator)
extension NotifyClient {

public var subscriptionChangedPublisher: AnyPublisher<[NotifySubscription], Never> {
return notifySubscriptionsChangedRequestSubscriber.subscriptionChangedPublisher
}

public func register(deviceToken: String) async throws {
try await pushClient.register(deviceToken: deviceToken)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public struct NotifyClientFactory {
let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychainStorage, logger: logger)
let notifyMessageSubscriber = NotifyMessageSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, notifyStorage: notifyStorage, crypto: crypto, logger: logger)
let webDidResolver = NotifyWebDidResolver()
let deleteNotifySubscriptionRequester = DeleteNotifySubscriptionRequester(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, webDidResolver: webDidResolver, kms: kms, logger: logger, notifyStorage: notifyStorage)
let deleteNotifySubscriptionRequester = DeleteNotifySubscriptionRequester(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, kms: kms, logger: logger, notifyStorage: notifyStorage)
let resubscribeService = NotifyResubscribeService(networkInteractor: networkInteractor, notifyStorage: notifyStorage, logger: logger)

let notifyConfigProvider = NotifyConfigProvider(projectId: projectId, explorerHost: explorerHost)
Expand All @@ -56,7 +56,7 @@ public struct NotifyClientFactory {

let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifyConfigProvider: notifyConfigProvider)

let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, webDidResolver: webDidResolver, identityClient: identityClient, networkingInteractor: networkInteractor, notifyConfigProvider: notifyConfigProvider, logger: logger, notifyStorage: notifyStorage)
let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, identityClient: identityClient, networkingInteractor: networkInteractor, notifyConfigProvider: notifyConfigProvider, logger: logger, notifyStorage: notifyStorage)

let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, notifyConfigProvider: notifyConfigProvider, notifyStorage: notifyStorage)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ final class NotifyStorage: NotifyStoring {

func updateSubscription(_ subscription: NotifySubscription, scope: [String: ScopeValue], expiry: UInt64) {
let expiry = Date(timeIntervalSince1970: TimeInterval(expiry))
let updated = NotifySubscription(topic: subscription.topic, account: subscription.account, relay: subscription.relay, metadata: subscription.metadata, scope: scope, expiry: expiry, symKey: subscription.symKey)
let updated = NotifySubscription(topic: subscription.topic, account: subscription.account, relay: subscription.relay, metadata: subscription.metadata, scope: scope, expiry: expiry, symKey: subscription.symKey, appAuthenticationKey: subscription.appAuthenticationKey)
subscriptionStore.set(element: updated, for: updated.account.absoluteString)
updateSubscriptionSubject.send(updated)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ class NotifySubscriptionsBuilder {
metadata: config.metadata,
scope: scope,
expiry: subscription.expiry,
symKey: subscription.symKey
symKey: subscription.symKey,
appAuthenticationKey: subscription.appAuthenticationKey
))
} catch {
continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@ final class NotifyWebDidResolver {
private static var subscribeKey = "wc-notify-subscribe-key"
private static var authenticationKey = "wc-notify-authentication-key"

func resolveAgreementKey(domain: String) async throws -> AgreementPublicKey {
let didDoc = try await resolveDidDoc(domainUrl: domain)
let subscribeKeyPath = "\(didDoc.id)#\(Self.subscribeKey)"
guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == subscribeKeyPath }) else { throw Errors.noVerificationMethodForKey }
guard verificationMethod.publicKeyJwk.crv == .X25519 else { throw Errors.unsupportedCurve}
let pubKeyBase64Url = verificationMethod.publicKeyJwk.x
return try AgreementPublicKey(base64url: pubKeyBase64Url)
func resolveDidDoc(appDomain: String) async throws -> WebDidDoc {
guard let didDocUrl = URL(string: "https://\(appDomain)/.well-known/did.json") else { throw Errors.invalidUrl }
let (data, _) = try await URLSession.shared.data(from: didDocUrl)
return try JSONDecoder().decode(WebDidDoc.self, from: data)
}

// TODO - Add cache for diddocs
func resolveAgreementKey(didDoc: WebDidDoc) throws -> AgreementPublicKey {
let keypath = "\(didDoc.id)#\(Self.subscribeKey)"
let pubKeyBase64Url = try resolveKey(didDoc: didDoc, curve: .X25519, keyPath: keypath)
return try AgreementPublicKey(base64url: pubKeyBase64Url)
}

func resolveAuthenticationKey(domain: String) async throws -> Data {
let didDoc = try await resolveDidDoc(domainUrl: domain)
let authenticationKeyPath = "\(didDoc.id)#\(Self.authenticationKey)"
guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == authenticationKeyPath }) else { throw Errors.noVerificationMethodForKey }
guard verificationMethod.publicKeyJwk.crv == .Ed25519 else { throw Errors.unsupportedCurve}
let pubKeyBase64Url = verificationMethod.publicKeyJwk.x
func resolveAuthenticationKey(didDoc: WebDidDoc) throws -> Data {
let keyPath = "\(didDoc.id)#\(Self.authenticationKey)"
let pubKeyBase64Url = try resolveKey(didDoc: didDoc, curve: .Ed25519, keyPath: keyPath)
guard let raw = Data(base64url: pubKeyBase64Url) else { throw Errors.invalidBase64urlString }
return raw
}
Expand All @@ -36,9 +34,9 @@ private extension NotifyWebDidResolver {
case unsupportedCurve
}

func resolveDidDoc(domainUrl: String) async throws -> WebDidDoc {
guard let didDocUrl = URL(string: "https://\(domainUrl)/.well-known/did.json") else { throw Errors.invalidUrl }
let (data, _) = try await URLSession.shared.data(from: didDocUrl)
return try JSONDecoder().decode(WebDidDoc.self, from: data)
func resolveKey(didDoc: WebDidDoc, curve: WebDidDoc.PublicKeyJwk.Curve, keyPath: String) throws -> String {
guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == keyPath }) else { throw Errors.noVerificationMethodForKey }
guard verificationMethod.publicKeyJwk.crv == curve else { throw Errors.unsupportedCurve }
return verificationMethod.publicKeyJwk.x
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ class DeleteNotifySubscriptionRequester {
private let keyserver: URL
private let networkingInteractor: NetworkInteracting
private let identityClient: IdentityClient
private let webDidResolver: NotifyWebDidResolver
private let kms: KeyManagementServiceProtocol
private let logger: ConsoleLogging
private let notifyStorage: NotifyStorage
Expand All @@ -16,15 +15,13 @@ class DeleteNotifySubscriptionRequester {
keyserver: URL,
networkingInteractor: NetworkInteracting,
identityClient: IdentityClient,
webDidResolver: NotifyWebDidResolver,
kms: KeyManagementServiceProtocol,
logger: ConsoleLogging,
notifyStorage: NotifyStorage
) {
self.keyserver = keyserver
self.networkingInteractor = networkingInteractor
self.identityClient = identityClient
self.webDidResolver = webDidResolver
self.kms = kms
self.logger = logger
self.notifyStorage = notifyStorage
Expand All @@ -37,10 +34,10 @@ class DeleteNotifySubscriptionRequester {
else { throw Errors.notifySubscriptionNotFound}

let protocolMethod = NotifyDeleteProtocolMethod()
let dappAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: subscription.metadata.url)
let dappAuthenticationKey = try DIDKey(did: subscription.appAuthenticationKey)

let wrapper = try createJWTWrapper(
dappPubKey: DIDKey(rawData: dappAuthenticationKey),
dappPubKey: dappAuthenticationKey,
reason: NotifyDeleteParams.userDisconnected.message,
app: DIDWeb(host: subscription.metadata.url),
account: subscription.account
Expand All @@ -57,11 +54,7 @@ class DeleteNotifySubscriptionRequester {
logger.debug("Subscription removed, topic: \(topic)")

kms.deleteSymmetricKey(for: topic)
}




}
}

private extension DeleteNotifySubscriptionRequester {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ class NotifySubscriptionsChangedRequestSubscriber {
private let notifyStorage: NotifyStorage
private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder

private let subscriptionChangedSubject = PassthroughSubject<[NotifySubscription], Never>()

var subscriptionChangedPublisher: AnyPublisher<[NotifySubscription], Never> {
return subscriptionChangedSubject.eraseToAnyPublisher()
}

init(
keyserver: URL,
networkingInteractor: NetworkInteracting,
Expand Down Expand Up @@ -47,37 +53,39 @@ class NotifySubscriptionsChangedRequestSubscriber {

let oldSubscriptions = notifyStorage.getSubscriptions(account: account)
let newSubscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(jwtPayload.subscriptions)


subscriptionChangedSubject.send(newSubscriptions)

try Task.checkCancellation()

let subscriptions = oldSubscriptions.difference(from: newSubscriptions)

logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)")

guard subscriptions.count > 0 else { return }
if subscriptions.count > 0 {
notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account)

notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account)
for subscription in newSubscriptions {
let symKey = try SymmetricKey(hex: subscription.symKey)
try groupKeychainStorage.add(symKey, forKey: subscription.topic)
try kms.setSymmetricKey(symKey, for: subscription.topic)
}

for subscription in newSubscriptions {
let symKey = try SymmetricKey(hex: subscription.symKey)
try groupKeychainStorage.add(symKey, forKey: subscription.topic)
try kms.setSymmetricKey(symKey, for: subscription.topic)
}
let topics = newSubscriptions.map { $0.topic }

let topics = newSubscriptions.map { $0.topic }
try await networkingInteractor.batchSubscribe(topics: topics)

try await networkingInteractor.batchSubscribe(topics: topics)
try Task.checkCancellation()

try Task.checkCancellation()
var logProperties = ["rpcId": payload.id.string]
for (index, subscription) in newSubscriptions.enumerated() {
let key = "subscription_\(index + 1)"
logProperties[key] = subscription.topic
}

var logProperties = ["rpcId": payload.id.string]
for (index, subscription) in newSubscriptions.enumerated() {
let key = "subscription_\(index + 1)"
logProperties[key] = subscription.topic
logger.debug("Updated Subscriptions by Subscriptions Changed Request", properties: logProperties)
}

logger.debug("Updated Subscriptions by Subscriptions Changed Request", properties: logProperties)

try await respond(topic: payload.topic, account: jwtPayload.account, rpcId: payload.id, notifyServerAuthenticationKey: jwtPayload.notifyServerAuthenticationKey)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class NotifyUpdateRequester: NotifyUpdateRequesting {
}

private let keyserverURL: URL
private let webDidResolver: NotifyWebDidResolver
private let identityClient: IdentityClient
private let networkingInteractor: NetworkInteracting
private let notifyConfigProvider: NotifyConfigProvider
Expand All @@ -19,15 +18,13 @@ class NotifyUpdateRequester: NotifyUpdateRequesting {

init(
keyserverURL: URL,
webDidResolver: NotifyWebDidResolver,
identityClient: IdentityClient,
networkingInteractor: NetworkInteracting,
notifyConfigProvider: NotifyConfigProvider,
logger: ConsoleLogging,
notifyStorage: NotifyStorage
) {
self.keyserverURL = keyserverURL
self.webDidResolver = webDidResolver
self.identityClient = identityClient
self.networkingInteractor = networkingInteractor
self.notifyConfigProvider = notifyConfigProvider
Expand All @@ -40,10 +37,10 @@ class NotifyUpdateRequester: NotifyUpdateRequesting {

guard let subscription = notifyStorage.getSubscription(topic: topic) else { throw Errors.noSubscriptionForGivenTopic }

let dappAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: subscription.metadata.url)
let dappAuthenticationKey = try DIDKey(did: subscription.appAuthenticationKey)

let request = try createJWTRequest(
dappPubKey: DIDKey(rawData: dappAuthenticationKey),
dappPubKey: dappAuthenticationKey,
subscriptionAccount: subscription.account,
appDomain: subscription.metadata.url, scope: scope
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ class NotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting {

logger.debug("Watching subscriptions")

let notifyServerPublicKey = try await webDidResolver.resolveAgreementKey(domain: notifyHost)
let notifyServerAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: notifyHost)
let didDoc = try await webDidResolver.resolveDidDoc(appDomain: notifyHost)
let notifyServerPublicKey = try webDidResolver.resolveAgreementKey(didDoc: didDoc)
let notifyServerAuthenticationKey = try webDidResolver.resolveAuthenticationKey(didDoc: didDoc)

let notifyServerAuthenticationDidKey = DIDKey(rawData: notifyServerAuthenticationKey)
let watchSubscriptionsTopic = notifyServerPublicKey.rawRepresentation.sha256().toHexString()

Expand Down
Loading

0 comments on commit f834829

Please sign in to comment.