import CryptoKit import Foundation struct DeviceIdentity: Codable, Sendable { var deviceId: String var publicKey: String var privateKey: String var createdAtMs: Int } enum DeviceIdentityStore { private static let fileName = "device.json" static func loadOrCreate() -> DeviceIdentity { let url = self.fileURL() if let data = try? Data(contentsOf: url), let decoded = try? JSONDecoder().decode(DeviceIdentity.self, from: data), !decoded.deviceId.isEmpty, !decoded.publicKey.isEmpty, !decoded.privateKey.isEmpty { return decoded } let identity = self.generate() self.save(identity) return identity } static func signPayload(_ payload: String, identity: DeviceIdentity) -> String? { guard let privateKeyData = Data(base64Encoded: identity.privateKey) else { return nil } do { let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKeyData) let signature = try privateKey.signature(for: Data(payload.utf8)) return self.base64UrlEncode(signature) } catch { return nil } } private static func generate() -> DeviceIdentity { let privateKey = Curve25519.Signing.PrivateKey() let publicKey = privateKey.publicKey let publicKeyData = publicKey.rawRepresentation let privateKeyData = privateKey.rawRepresentation let deviceId = SHA256.hash(data: publicKeyData).compactMap { String(format: "%02x", $0) }.joined() return DeviceIdentity( deviceId: deviceId, publicKey: publicKeyData.base64EncodedString(), privateKey: privateKeyData.base64EncodedString(), createdAtMs: Int(Date().timeIntervalSince1970 * 1000)) } private static func base64UrlEncode(_ data: Data) -> String { let base64 = data.base64EncodedString() return base64 .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "=", with: "") } static func publicKeyBase64Url(_ identity: DeviceIdentity) -> String? { guard let data = Data(base64Encoded: identity.publicKey) else { return nil } return self.base64UrlEncode(data) } private static func save(_ identity: DeviceIdentity) { let url = self.fileURL() do { try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true) let data = try JSONEncoder().encode(identity) try data.write(to: url, options: [.atomic]) } catch { // best-effort only } } private static func fileURL() -> URL { let base = ClawdbotPaths.stateDirURL return base .appendingPathComponent("identity", isDirectory: true) .appendingPathComponent(fileName, isDirectory: false) } }