Files
clawdbot/apps/macos/Sources/Clawdbot/DeviceIdentity.swift
2026-01-19 10:07:56 +00:00

85 lines
3.0 KiB
Swift

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)
}
}