// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import Sodium import SessionUtilitiesKit public class UpdateExpiryResponse: SnodeRecursiveResponse {} public struct UpdateExpiryResponseResult { public let changed: [String: UInt64] public let unchanged: [String: UInt64] public let didError: Bool } // MARK: - SwarmItem public extension UpdateExpiryResponse { class SwarmItem: SnodeSwarmItem { private enum CodingKeys: String, CodingKey { case updated case unchanged case expiry } public let updated: [String] public let unchanged: [String: UInt64] public let expiry: UInt64? // MARK: - Initialization required init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) updated = ((try? container.decode([String].self, forKey: .updated)) ?? []) unchanged = ((try? container.decode([String: UInt64].self, forKey: .unchanged)) ?? [:]) expiry = try? container.decode(UInt64.self, forKey: .expiry) try super.init(from: decoder) } } } // MARK: - ValidatableResponse extension UpdateExpiryResponse: ValidatableResponse { typealias ValidationData = [String] typealias ValidationResponse = UpdateExpiryResponseResult /// All responses in the swarm must be valid internal static var requiredSuccessfulResponses: Int { -1 } internal func validResultMap( sodium: Sodium, userX25519PublicKey: String, validationData: [String] ) throws -> [String: UpdateExpiryResponseResult] { let validationMap: [String: UpdateExpiryResponseResult] = try swarm.reduce(into: [:]) { result, next in guard !next.value.failed, let appliedExpiry: UInt64 = next.value.expiry, let signatureBase64: String = next.value.signatureBase64, let encodedSignature: Data = Data(base64Encoded: signatureBase64) else { result[next.key] = UpdateExpiryResponseResult(changed: [:], unchanged: [:], didError: true) if let reason: String = next.value.reason, let statusCode: Int = next.value.code { SNLog("Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).") } else { SNLog("Couldn't update expiry from: \(next.key).") } return } /// Signature of /// `( 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. The signature uses the node's ed25519 pubkey. /// /// **Note:** If `updated` is empty then the `expiry` value will match the value that was /// included in the original request let verificationBytes: [UInt8] = userX25519PublicKey.bytes .appending(contentsOf: "\(appliedExpiry)".data(using: .ascii)?.bytes) .appending(contentsOf: validationData.joined().bytes) .appending(contentsOf: next.value.updated.sorted().joined().bytes) .appending(contentsOf: next.value.unchanged .sorted(by: { lhs, rhs in lhs.key < rhs.key }) .reduce(into: [UInt8]()) { result, nextUnchanged in result.append(contentsOf: nextUnchanged.key.bytes) result.append(contentsOf: "\(nextUnchanged.value)".data(using: .ascii)?.bytes ?? []) } ) let isValid: Bool = sodium.sign.verify( message: verificationBytes, publicKey: Data(hex: next.key).bytes, signature: encodedSignature.bytes ) // If the update signature is invalid then we want to fail here guard isValid else { throw SnodeAPIError.signatureVerificationFailed } result[next.key] = UpdateExpiryResponseResult( changed: next.value.updated.reduce(into: [:]) { prev, next in prev[next] = appliedExpiry }, unchanged: next.value.unchanged, didError: false ) } return try Self.validated(map: validationMap, totalResponseCount: swarm.count) } }