85 lines
3.0 KiB
Swift
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)
|
|
}
|
|
}
|