import Foundation /// Key bundle for one device, used in X3DH struct DeviceBundle { let deviceId: String let identityKey: Data // Ed25519 public key (32 bytes) let spk: Data // X25519 public key (32 bytes) let spkSignature: Data // Ed25519 signature (64 bytes) let spkId: String let opk: Data? // X25519 public key (32 bytes), optional let opkId: String? /// Parse from server response dictionary /// Server uses: signed_prekey, signed_prekey_id, one_time_prekey, one_time_prekey_id (base64) static func fromDict(_ dict: [String: Any], identityKey: Data? = nil) throws -> DeviceBundle { guard let deviceId = dict["device_id"] as? String else { throw ChatError.invalidData("Missing device_id") } // Identity key can be passed in (from parent) or in dict let ik: Data if let passedIk = identityKey { ik = passedIk } else if let ikB64 = dict["identity_key"] as? String, let ikData = Data(base64Encoded: ikB64) { ik = ikData } else { throw ChatError.invalidData("Missing identity_key") } // SPK - try both naming conventions, base64 encoded let spkB64 = dict["signed_prekey"] as? String ?? dict["spk"] as? String guard let spkB64 = spkB64, let spk = Data(base64Encoded: spkB64) else { throw ChatError.invalidData("Missing signed_prekey") } // SPK signature - base64 encoded guard let spkSigB64 = dict["spk_signature"] as? String, let spkSig = Data(base64Encoded: spkSigB64) else { throw ChatError.invalidData("Missing spk_signature") } // SPK ID - try both naming conventions let spkId = dict["signed_prekey_id"] as? String ?? dict["spk_id"] as? String guard let spkId = spkId else { throw ChatError.invalidData("Missing signed_prekey_id") } // OPK - optional, base64 encoded var opk: Data? var opkId: String? let opkB64 = dict["one_time_prekey"] as? String ?? dict["opk"] as? String if let opkB64 = opkB64, let opkData = Data(base64Encoded: opkB64) { opk = opkData opkId = dict["one_time_prekey_id"] as? String ?? dict["opk_id"] as? String } return DeviceBundle( deviceId: deviceId, identityKey: ik, spk: spk, spkSignature: spkSig, spkId: spkId, opk: opk, opkId: opkId ) } }