feat: unify device auth + pairing
This commit is contained in:
84
apps/macos/Sources/Clawdbot/DeviceIdentity.swift
Normal file
84
apps/macos/Sources/Clawdbot/DeviceIdentity.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user