eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import AuthenticationServices
2import Foundation
3
4#if canImport(UIKit)
5import UIKit
6public typealias HomeAuthAnchor = UIWindow
7#elseif canImport(AppKit)
8import AppKit
9public typealias HomeAuthAnchor = NSWindow
10#endif
11
12// đŸ¤– The native "Sign in with Eleven" passkey ceremony (AuthenticationServices,
13// iOS 18 / macOS 15 for the PRF extension). It runs a discoverable login (or a
14// registration for a first backup), evaluates the SAME PRF input the web uses,
15// reshapes Apple's raw ceremony fields into the WebAuthn JSON the go-webauthn
16// server verifies, and returns the link token + credential id + PRF output.
17// It never sends the PRF output anywhere — that stays here for HomeCrypto.
18@available(iOS 18.0, macOS 15.0, *)
19public final class HomeAuth: NSObject {
20 public struct Result {
21 public let token: String
22 public let proof: String?
23 public let credentialID: String // base64url
24 public let prf: Data
25 }
26
27 private let client: HomeClient
28 private let rpID: String
29 private let anchor: HomeAuthAnchor
30 private var continuation: CheckedContinuation<ASAuthorization, Error>?
31
32 public init(client: HomeClient = HomeClient(),
33 rpID: String = HomeBase.rpID,
34 anchor: HomeAuthAnchor) {
35 self.client = client
36 self.rpID = rpID
37 self.anchor = anchor
38 }
39
40 // ---- sign in (discoverable assertion) -----------------------------------
41
42 public func signIn() async throws -> Result {
43 let begin = try await client.loginBegin()
44 guard let challenge = Data(base64urlEncoded: begin.options.publicKey.challenge) else {
45 throw HomeAuthError.badOptions
46 }
47 let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpID)
48 let request = provider.createCredentialAssertionRequest(challenge: challenge)
49 request.prf = ASAuthorizationPublicKeyCredentialPRFAssertionInput.inputValues(.init(saltInput1: HomeCrypto.prfEvalInput))
50
51 let auth = try await run(request)
52 guard let cred = auth.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else {
53 throw HomeAuthError.wrongCredential
54 }
55 // Apple's Swift overlay hands the PRF output back as a SymmetricKey; its
56 // raw bytes are exactly what the web feeds HKDF, so byte-compat holds.
57 guard let prfKey = cred.prf?.first else { throw HomeAuthError.noPRF }
58 let prf = prfKey.withUnsafeBytes { Data($0) }
59
60 let json = try Self.assertionJSON(cred)
61 let fin = try await client.finish(register: false, session: begin.session, credentialJSON: json)
62 return Result(token: fin.token, proof: fin.proof,
63 credentialID: fin.credential_id, prf: prf)
64 }
65
66 // ---- register a first backup (attestation) ------------------------------
67
68 public func register() async throws -> Result {
69 let begin = try await client.registerBegin()
70 guard let challenge = Data(base64urlEncoded: begin.options.publicKey.challenge),
71 let userIDB64 = begin.options.publicKey.user?.id,
72 let userID = Data(base64urlEncoded: userIDB64) else {
73 throw HomeAuthError.badOptions
74 }
75 let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpID)
76 let request = provider.createCredentialRegistrationRequest(
77 challenge: challenge, name: "Eleven", userID: userID)
78 request.prf = ASAuthorizationPublicKeyCredentialPRFRegistrationInput.inputValues(
79 .init(saltInput1: HomeCrypto.prfEvalInput))
80
81 let auth = try await run(request)
82 guard let cred = auth.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration else {
83 throw HomeAuthError.wrongCredential
84 }
85 let json = try Self.registrationJSON(cred)
86 let fin = try await client.finish(register: true, session: begin.session, credentialJSON: json)
87
88 // Most authenticators evaluate PRF at create time; if not, one assertion
89 // with the same input recovers it (mirrors backup-core.js runRegister).
90 var prf = cred.prf?.first.map { $0.withUnsafeBytes { Data($0) } }
91 if prf == nil {
92 prf = try await prfViaAssertion(credentialID: cred.credentialID)
93 }
94 guard let prf else { throw HomeAuthError.noPRF }
95 return Result(token: fin.token, proof: fin.proof,
96 credentialID: fin.credential_id, prf: prf)
97 }
98
99 private func prfViaAssertion(credentialID: Data) async throws -> Data? {
100 let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpID)
101 var localChallenge = Data(count: 32)
102 localChallenge.withUnsafeMutableBytes { _ = SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!) }
103 let request = provider.createCredentialAssertionRequest(challenge: localChallenge)
104 request.allowedCredentials = [
105 ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: credentialID)]
106 request.prf = ASAuthorizationPublicKeyCredentialPRFAssertionInput.inputValues(.init(saltInput1: HomeCrypto.prfEvalInput))
107 let auth = try await run(request)
108 guard let key = (auth.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion)?
109 .prf?.first else { return nil }
110 return key.withUnsafeBytes { Data($0) }
111 }
112
113 // ---- WebAuthn JSON assembly (go-webauthn wire) --------------------------
114
115 private static func registrationJSON(_ c: ASAuthorizationPlatformPublicKeyCredentialRegistration) throws -> Data {
116 let id = c.credentialID.base64urlEncodedString()
117 var response: [String: Any] = [
118 "clientDataJSON": c.rawClientDataJSON.base64urlEncodedString(),
119 ]
120 if let att = c.rawAttestationObject {
121 response["attestationObject"] = att.base64urlEncodedString()
122 }
123 return try JSONSerialization.data(withJSONObject: [
124 "id": id, "rawId": id, "type": "public-key",
125 "clientExtensionResults": [String: Any](),
126 "response": response,
127 ])
128 }
129
130 private static func assertionJSON(_ c: ASAuthorizationPlatformPublicKeyCredentialAssertion) throws -> Data {
131 let id = c.credentialID.base64urlEncodedString()
132 var response: [String: Any] = [
133 "clientDataJSON": c.rawClientDataJSON.base64urlEncodedString(),
134 "authenticatorData": c.rawAuthenticatorData.base64urlEncodedString(),
135 "signature": c.signature.base64urlEncodedString(),
136 ]
137 if !c.userID.isEmpty { response["userHandle"] = c.userID.base64urlEncodedString() }
138 return try JSONSerialization.data(withJSONObject: [
139 "id": id, "rawId": id, "type": "public-key",
140 "clientExtensionResults": [String: Any](),
141 "response": response,
142 ])
143 }
144
145 // ---- controller driving -------------------------------------------------
146
147 private func run(_ request: ASAuthorizationRequest) async throws -> ASAuthorization {
148 try await withCheckedThrowingContinuation { cont in
149 self.continuation = cont
150 let controller = ASAuthorizationController(authorizationRequests: [request])
151 controller.delegate = self
152 controller.presentationContextProvider = self
153 controller.performRequests()
154 }
155 }
156}
157
158@available(iOS 18.0, macOS 15.0, *)
159extension HomeAuth: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
160 public func authorizationController(controller: ASAuthorizationController,
161 didCompleteWithAuthorization authorization: ASAuthorization) {
162 continuation?.resume(returning: authorization)
163 continuation = nil
164 }
165
166 public func authorizationController(controller: ASAuthorizationController,
167 didCompleteWithError error: Error) {
168 continuation?.resume(throwing: error)
169 continuation = nil
170 }
171
172 public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
173 anchor
174 }
175}
176
177public enum HomeAuthError: Error { case badOptions, wrongCredential, noPRF }
178