// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import SessionSnodeKit extension PushNotificationAPI { struct SubscribeRequest: Encodable { struct ServiceInfo: Codable { private enum CodingKeys: String, CodingKey { case token } private let token: String // MARK: - Initialization init(token: String) { self.token = token } } private enum CodingKeys: String, CodingKey { case pubkey case ed25519PublicKey = "session_ed25519" case subkey = "subkey_tag" case namespaces case includeMessageData = "data" case timestamp = "sig_ts" case signatureBase64 = "signature" case service case serviceInfo = "service_info" case notificationsEncryptionKey = "enc_key" } /// The 33-byte account being subscribed to; typically a session ID. private let pubkey: String /// List of integer namespace (-32768 through 32767). These must be sorted in ascending order. private let namespaces: [SnodeAPI.Namespace] /// If provided and true then notifications will include the body of the message (as long as it isn't too large); if false then the body will /// not be included in notifications. private let includeMessageData: Bool /// Dict of service-specific data; typically this includes just a "token" field with a device-specific token, but different services in the /// future may have different input requirements. private let serviceInfo: ServiceInfo /// 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 using this key. Though /// it is permitted for this to change, it is recommended that the device generate this once and persist it. private let notificationsEncryptionKey: Data /// 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth private let subkey: String? /// The signature unix timestamp (seconds, not ms) private let timestamp: Int64 /// When the pubkey value starts with 05 (i.e. a session ID) this is the underlying ed25519 32-byte pubkey associated with the session /// ID. When not 05, this field should not be provided. private let ed25519PublicKey: [UInt8] /// Secret key used to generate the signature (**Not** sent with the request) private let ed25519SecretKey: [UInt8] // MARK: - Initialization init( pubkey: String, namespaces: [SnodeAPI.Namespace], includeMessageData: Bool, serviceInfo: ServiceInfo, notificationsEncryptionKey: Data, subkey: String?, timestamp: TimeInterval, ed25519PublicKey: [UInt8], ed25519SecretKey: [UInt8] ) { self.pubkey = pubkey self.namespaces = namespaces self.includeMessageData = includeMessageData self.serviceInfo = serviceInfo self.notificationsEncryptionKey = notificationsEncryptionKey self.subkey = subkey self.timestamp = Int64(timestamp) // Server expects rounded seconds self.ed25519PublicKey = ed25519PublicKey self.ed25519SecretKey = ed25519SecretKey } // MARK: - Coding public func encode(to encoder: Encoder) throws { var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) // Generate the signature for the request for encoding let signatureBase64: String = try generateSignature().toBase64() try container.encode(pubkey, forKey: .pubkey) try container.encode(ed25519PublicKey.toHexString(), forKey: .ed25519PublicKey) try container.encodeIfPresent(subkey, forKey: .subkey) try container.encode(namespaces.map { $0.rawValue}.sorted(), forKey: .namespaces) try container.encode(includeMessageData, forKey: .includeMessageData) try container.encode(timestamp, forKey: .timestamp) try container.encode(signatureBase64, forKey: .signatureBase64) try container.encode(Service.apns, forKey: .service) try container.encode(serviceInfo, forKey: .serviceInfo) try container.encode(notificationsEncryptionKey.toHexString(), forKey: .notificationsEncryptionKey) } // MARK: - Abstract Methods func generateSignature() throws -> [UInt8] { /// The signature data collected and stored here is used by the PN server to subscribe to the swarms /// for the given account; the specific rules are governed by the storage server, but in general: /// /// A signature must have been produced (via the timestamp) within the past 14 days. It is /// recommended that clients generate a new signature whenever they re-subscribe, and that /// re-subscriptions happen more frequently than once every 14 days. /// /// A signature is signed using the account's Ed25519 private key (or Ed25519 subkey, if using /// subkey authentication with a `subkey_tag`, for future closed group subscriptions), and signs the value: /// `"MONITOR" || HEX(ACCOUNT) || SIG_TS || DATA01 || NS[0] || "," || ... || "," || NS[n]` /// /// Where `SIG_TS` is the `sig_ts` value as a base-10 string; `DATA01` is either "0" or "1" depending /// on whether the subscription wants message data included; and the trailing `NS[i]` values are a /// comma-delimited list of namespaces that should be subscribed to, in the same sorted order as /// the `namespaces` parameter. let verificationBytes: [UInt8] = "MONITOR".bytes .appending(contentsOf: pubkey.bytes) .appending(contentsOf: "\(timestamp)".bytes) .appending(contentsOf: (includeMessageData ? "1" : "0").bytes) .appending( contentsOf: namespaces .map { $0.rawValue } // Intentionally not using `verificationString` here .sorted() .map { "\($0)" } .joined(separator: ",") .bytes ) // TODO: Need to add handling for subkey auth guard let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( message: verificationBytes, secretKey: ed25519SecretKey ) else { throw SnodeAPIError.signingFailed } return signatureBytes } } }