Skip to content

Commit 73e9e78

Browse files
committed
feat: unify device auth + pairing
1 parent 47d1f23 commit 73e9e78

30 files changed

Lines changed: 2041 additions & 20 deletions
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import CryptoKit
2+
import Foundation
3+
4+
struct DeviceIdentity: Codable, Sendable {
5+
var deviceId: String
6+
var publicKey: String
7+
var privateKey: String
8+
var createdAtMs: Int
9+
}
10+
11+
enum DeviceIdentityStore {
12+
private static let fileName = "device.json"
13+
14+
static func loadOrCreate() -> DeviceIdentity {
15+
let url = self.fileURL()
16+
if let data = try? Data(contentsOf: url),
17+
let decoded = try? JSONDecoder().decode(DeviceIdentity.self, from: data),
18+
!decoded.deviceId.isEmpty,
19+
!decoded.publicKey.isEmpty,
20+
!decoded.privateKey.isEmpty {
21+
return decoded
22+
}
23+
let identity = self.generate()
24+
self.save(identity)
25+
return identity
26+
}
27+
28+
static func signPayload(_ payload: String, identity: DeviceIdentity) -> String? {
29+
guard let privateKeyData = Data(base64Encoded: identity.privateKey) else { return nil }
30+
do {
31+
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKeyData)
32+
let signature = try privateKey.signature(for: Data(payload.utf8))
33+
return self.base64UrlEncode(signature)
34+
} catch {
35+
return nil
36+
}
37+
}
38+
39+
private static func generate() -> DeviceIdentity {
40+
let privateKey = Curve25519.Signing.PrivateKey()
41+
let publicKey = privateKey.publicKey
42+
let publicKeyData = publicKey.rawRepresentation
43+
let privateKeyData = privateKey.rawRepresentation
44+
let deviceId = SHA256.hash(data: publicKeyData).compactMap { String(format: "%02x", $0) }.joined()
45+
return DeviceIdentity(
46+
deviceId: deviceId,
47+
publicKey: publicKeyData.base64EncodedString(),
48+
privateKey: privateKeyData.base64EncodedString(),
49+
createdAtMs: Int(Date().timeIntervalSince1970 * 1000))
50+
}
51+
52+
private static func base64UrlEncode(_ data: Data) -> String {
53+
let base64 = data.base64EncodedString()
54+
return base64
55+
.replacingOccurrences(of: "+", with: "-")
56+
.replacingOccurrences(of: "/", with: "_")
57+
.replacingOccurrences(of: "=", with: "")
58+
}
59+
60+
static func publicKeyBase64Url(_ identity: DeviceIdentity) -> String? {
61+
guard let data = Data(base64Encoded: identity.publicKey) else { return nil }
62+
return self.base64UrlEncode(data)
63+
}
64+
65+
private static func save(_ identity: DeviceIdentity) {
66+
let url = self.fileURL()
67+
do {
68+
try FileManager.default.createDirectory(
69+
at: url.deletingLastPathComponent(),
70+
withIntermediateDirectories: true)
71+
let data = try JSONEncoder().encode(identity)
72+
try data.write(to: url, options: [.atomic])
73+
} catch {
74+
// best-effort only
75+
}
76+
}
77+
78+
private static func fileURL() -> URL {
79+
let base = ClawdbotPaths.stateDirURL
80+
return base
81+
.appendingPathComponent("identity", isDirectory: true)
82+
.appendingPathComponent(fileName, isDirectory: false)
83+
}
84+
}

0 commit comments

Comments
 (0)