From 66b33b6623b04db383f02028d4162ca328c7ea26 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 16 Jan 2023 11:15:12 +1100 Subject: [PATCH] update `expire` endpoint --- SessionSnodeKit/Models/SnodeAPIEndpoint.swift | 1 + SessionSnodeKit/SnodeAPI.swift | 37 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/SessionSnodeKit/Models/SnodeAPIEndpoint.swift b/SessionSnodeKit/Models/SnodeAPIEndpoint.swift index 63ffd5334..e12d66388 100644 --- a/SessionSnodeKit/Models/SnodeAPIEndpoint.swift +++ b/SessionSnodeKit/Models/SnodeAPIEndpoint.swift @@ -11,6 +11,7 @@ public enum SnodeAPIEndpoint: String { case getInfo = "info" case clearAllData = "delete_all" case expire = "expire" + case getExipires = "get_expiries" case batch = "batch" case sequence = "sequence" } diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 42262b394..611354e88 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -735,21 +735,35 @@ public final class SnodeAPI { public static func updateExpiry( publicKey: String, updatedExpiryMs: Int64, - serverHashes: [String] - ) -> Promise<[String: (hashes: [String], expiry: UInt64)]> { + serverHashes: [String], + shortenOnly: Bool = true, + extendOnly: Bool = false + ) -> Promise<[String: (hashes: [String], expiry: UInt64, unchanged: [String: UInt64])]> { guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else { return Promise(error: SnodeAPIError.noKeyPair) } + // ShortenOnly and extendOnly cannot be true at the same time + guard !(shortenOnly && extendOnly) else { + return Promise(error: SnodeAPIError.generic) + } + let publicKey = (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey) let updatedExpiryMsWithNetworkOffset: UInt64 = UInt64(updatedExpiryMs + SnodeAPI.clockOffsetMs.wrappedValue) + let shortenOrExtend: String? = { + if shortenOnly { return "shorten" } + if extendOnly { return "extend" } + return nil + }() + return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { getSwarm(for: publicKey) - .then2 { swarm -> Promise<[String: (hashes: [String], expiry: UInt64)]> in - // "expire" || expiry || messages[0] || ... || messages[N] + .then2 { swarm -> Promise<[String: (hashes: [String], expiry: UInt64, unchanged: [String: UInt64])]> in + // "expire" || ShortenOrExtend || expiry || messages[0] || ... || messages[N] let verificationBytes = SnodeAPIEndpoint.expire.rawValue.bytes + .appending(contentsOf: shortenOrExtend?.data(using: .ascii)?.bytes) .appending(contentsOf: "\(updatedExpiryMsWithNetworkOffset)".data(using: .ascii)?.bytes) .appending(contentsOf: serverHashes.joined().bytes) @@ -773,13 +787,13 @@ public final class SnodeAPI { return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { invoke(.expire, on: snode, associatedWith: publicKey, parameters: parameters) - .map2 { responseData -> [String: (hashes: [String], expiry: UInt64)] in + .map2 { responseData -> [String: (hashes: [String], expiry: UInt64, unchanged: [String: UInt64])] in guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { throw HTTP.Error.invalidJSON } guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON } - var result: [String: (hashes: [String], expiry: UInt64)] = [:] + var result: [String: (hashes: [String], expiry: UInt64, unchanged: [String: UInt64])] = [:] for (snodePublicKey, rawJSON) in swarm { guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON } @@ -790,23 +804,28 @@ public final class SnodeAPI { else { SNLog("Couldn't delete data from: \(snodePublicKey).") } - result[snodePublicKey] = ([], 0) + result[snodePublicKey] = ([], 0, [:]) continue } guard let hashes: [String] = json["updated"] as? [String], + let unchanged: [String: UInt64] = json["unchanged"] as? [String: UInt64], let expiryApplied: UInt64 = json["expiry"] as? UInt64, let signature: String = json["signature"] as? String else { throw HTTP.Error.invalidJSON } - // The signature format is ( PUBKEY_HEX || EXPIRY || RMSG[0] || ... || RMSG[N] || UMSG[0] || ... || UMSG[M] ) + // The signature format is ( PUBKEY_HEX || EXPIRY || RMSGs... || UMSGs... || CMSG_EXPs... ) + // where RMSGs are the requested expiry hashes, UMSGs are the actual updated hashes, and + // CMSG_EXPs are (HASH || EXPIRY) values, ascii-sorted by hash, for the unchanged message + // hashes included in the "unchanged" field. let verificationBytes = publicKey.bytes .appending(contentsOf: "\(expiryApplied)".data(using: .ascii)?.bytes) .appending(contentsOf: serverHashes.joined().bytes) .appending(contentsOf: hashes.joined().bytes) + .appending(contentsOf: unchanged.map { "\($0)\($1)" }.sorted().joined().bytes) let isValid = sodium.sign.verify( message: verificationBytes, publicKey: Bytes(Data(hex: snodePublicKey)), @@ -818,7 +837,7 @@ public final class SnodeAPI { throw SnodeAPIError.signatureVerificationFailed } - result[snodePublicKey] = (hashes, expiryApplied) + result[snodePublicKey] = (hashes, expiryApplied, unchanged) } return result