2026-04-17 22:08:27 +02:00
import Foundation
2026-04-20 13:21:39 +00:00
import CryptoKit
#if canImport ( UIKit )
import UIKit
#endif
#if canImport ( CoreLocation )
import CoreLocation
#endif
#if canImport ( Security )
import Security
#endif
2026-04-17 22:08:27 +02:00
protocol IDPServicing {
func bootstrap ( ) async throws -> BootstrapContext
2026-04-18 01:05:22 +02:00
func signIn ( with request : PairingAuthenticationRequest ) async throws -> SignInResult
func identify ( with request : PairingAuthenticationRequest ) async throws -> DashboardSnapshot
2026-04-17 22:08:27 +02:00
func refreshDashboard ( ) async throws -> DashboardSnapshot
func approveRequest ( id : UUID ) async throws -> DashboardSnapshot
func rejectRequest ( id : UUID ) async throws -> DashboardSnapshot
func simulateIncomingRequest ( ) async throws -> DashboardSnapshot
func markNotificationRead ( id : UUID ) async throws -> DashboardSnapshot
}
2026-04-20 13:21:39 +00:00
enum DefaultIDPService {
static let shared : IDPServicing = LiveIDPService . shared
}
actor LiveIDPService : IDPServicing {
static let shared = LiveIDPService ( )
private let appStateStore : AppStateStoring
private let keyStore : PassportKeyStoring
2026-04-20 14:10:43 +00:00
private init (
2026-04-20 13:21:39 +00:00
appStateStore : AppStateStoring = UserDefaultsAppStateStore ( ) ,
keyStore : PassportKeyStoring = DefaultPassportKeyStore ( )
) {
self . appStateStore = appStateStore
self . keyStore = keyStore
}
func bootstrap ( ) async throws -> BootstrapContext {
let suggestedPayload = appStateStore . load ( ) ? . session . pairingCode ? ? " "
return BootstrapContext ( suggestedPairingPayload : suggestedPayload )
}
func signIn ( with request : PairingAuthenticationRequest ) async throws -> SignInResult {
try validateSignedGPSPosition ( in : request )
let context = try PairingPayloadParser . parse ( request . pairingPayload )
let baseURL = try serverURL ( from : context )
let client = LiveTypedRequestClient ( baseURL : baseURL )
let keyMaterial = try loadOrCreateKeyMaterial ( for : context . originHost )
let signingPayload = try buildEnrollmentSigningPayload ( from : context )
2026-04-20 14:10:43 +00:00
guard let privateKeyData = Data ( base64Encoded : keyMaterial . privateKeyBase64 ) else {
throw AppError . invalidPairingPayload
}
let signatureBase64 = try signPayload ( signingPayload , with : privateKeyData )
2026-04-20 13:21:39 +00:00
let enrollmentResponse = try await client . fire (
method : " completePassportEnrollment " ,
request : CompletePassportEnrollmentRequest (
pairingToken : context . pairingToken ,
deviceLabel : DeviceEnvironment . currentDeviceLabel ,
platform : DeviceEnvironment . currentPlatform ,
publicKeyX963Base64 : keyMaterial . publicKeyBase64 ,
signatureBase64 : signatureBase64 ,
signatureFormat : " der " ,
appVersion : Bundle . main . infoDictionary ? [ " CFBundleShortVersionString " ] as ? String ,
capabilities : DeviceEnvironment . capabilities
) ,
responseType : CompletePassportEnrollmentResponse . self
)
let session = AuthSession (
deviceName : enrollmentResponse . device . data . label ,
originHost : context . originHost ,
serverURL : baseURL . absoluteString ,
passportDeviceID : enrollmentResponse . device . id ,
pairedAt : . now ,
tokenPreview : context . tokenPreview ,
pairingCode : request . pairingPayload ,
pairingTransport : request . transport ,
signedGPSPosition : request . signedGPSPosition
)
let snapshot = try await dashboardSnapshot ( for : session )
return SignInResult ( session : session , snapshot : snapshot )
}
func identify ( with request : PairingAuthenticationRequest ) async throws -> DashboardSnapshot {
guard let state = appStateStore . load ( ) else {
throw AppError . invalidPairingPayload
}
_ = request
return try await dashboardSnapshot ( for : state . session )
}
func refreshDashboard ( ) async throws -> DashboardSnapshot {
guard let state = appStateStore . load ( ) else {
throw AppError . invalidPairingPayload
}
return try await dashboardSnapshot ( for : state . session )
}
func approveRequest ( id : UUID ) async throws -> DashboardSnapshot {
guard let state = appStateStore . load ( ) ,
let request = state . requests . first ( where : { $0 . id = = id } ) else {
throw AppError . requestNotFound
}
guard let signingPayload = request . signingPayload else {
throw AppError . requestNotFound
}
let client = LiveTypedRequestClient ( baseURL : URL ( string : state . session . serverURL ) ! )
let signatureBase64 = try signPayload ( signingPayload , with : try privateKeyData ( for : state . session ) )
let locationEvidence = request . requiresLocation
? try await CurrentLocationEvidenceProvider . currentLocationEvidenceIfAvailable ( )
: nil
_ = try await client . fire (
method : " approvePassportChallenge " ,
request : ApprovePassportChallengeRequest (
challengeId : request . serverID ,
deviceId : state . session . passportDeviceID ,
signatureBase64 : signatureBase64 ,
signatureFormat : " der " ,
location : locationEvidence ,
nfc : nil
) ,
responseType : ApprovePassportChallengeResponse . self
)
return try await dashboardSnapshot ( for : state . session )
}
func rejectRequest ( id : UUID ) async throws -> DashboardSnapshot {
guard let state = appStateStore . load ( ) ,
let request = state . requests . first ( where : { $0 . id = = id } ) else {
throw AppError . requestNotFound
}
let client = LiveTypedRequestClient ( baseURL : URL ( string : state . session . serverURL ) ! )
let signedRequest = try signedDeviceRequest (
session : state . session ,
action : " rejectPassportChallenge " ,
signedFields : [ " challenge_id= \( request . serverID ) " ]
)
_ = try await client . fire (
method : " rejectPassportChallenge " ,
request : RejectPassportChallengeRequest (
deviceId : signedRequest . deviceId ,
timestamp : signedRequest . timestamp ,
nonce : signedRequest . nonce ,
signatureBase64 : signedRequest . signatureBase64 ,
signatureFormat : signedRequest . signatureFormat ,
challengeId : request . serverID
) ,
responseType : RejectPassportChallengeResponse . self
)
return try await dashboardSnapshot ( for : state . session )
}
func simulateIncomingRequest ( ) async throws -> DashboardSnapshot {
try await refreshDashboard ( )
}
func markNotificationRead ( id : UUID ) async throws -> DashboardSnapshot {
guard let state = appStateStore . load ( ) ,
let notification = state . notifications . first ( where : { $0 . id = = id } ) else {
return try await refreshDashboard ( )
}
let client = LiveTypedRequestClient ( baseURL : URL ( string : state . session . serverURL ) ! )
let signedRequest = try signedDeviceRequest (
session : state . session ,
action : " markPassportAlertSeen " ,
signedFields : [ " hint_id= \( notification . hintID ) " ]
)
_ = try await client . fire (
method : " markPassportAlertSeen " ,
request : MarkPassportAlertSeenRequest (
deviceId : signedRequest . deviceId ,
timestamp : signedRequest . timestamp ,
nonce : signedRequest . nonce ,
signatureBase64 : signedRequest . signatureBase64 ,
signatureFormat : signedRequest . signatureFormat ,
hintId : notification . hintID
) ,
responseType : SimpleSuccessResponse . self
)
return try await dashboardSnapshot ( for : state . session )
}
private func dashboardSnapshot ( for session : AuthSession ) async throws -> DashboardSnapshot {
let client = LiveTypedRequestClient ( baseURL : URL ( string : session . serverURL ) ! )
let signedRequest = try signedDeviceRequest ( session : session , action : " getPassportDashboard " , signedFields : [ ] )
let response = try await client . fire (
method : " getPassportDashboard " ,
request : signedRequest ,
responseType : PassportDashboardResponse . self
)
return DashboardSnapshot (
profile : MemberProfile (
name : response . profile . name ,
handle : response . profile . handle ,
organization : response . profile . organizations . first ? . name ? ? " No organization " ,
deviceCount : response . profile . deviceCount ,
recoverySummary : response . profile . recoverySummary
) ,
requests : response . challenges . map { challengeItem in
ApprovalRequest (
serverID : challengeItem . challenge . id ,
hintID : challengeItem . challenge . data . notification ? . hintId ? ? challengeItem . challenge . id ,
title : approvalTitle ( for : challengeItem . challenge ) ,
subtitle : approvalSubtitle ( for : challengeItem . challenge ) ,
source : challengeItem . challenge . data . metadata . audience ? ? challengeItem . challenge . data . metadata . originHost ? ? " idp.global " ,
createdAt : Date ( millisecondsSince1970 : challengeItem . challenge . data . createdAt ) ,
kind : approvalKind ( for : challengeItem . challenge ) ,
risk : approvalRisk ( for : challengeItem . challenge ) ,
scopes : approvalScopes ( for : challengeItem . challenge ) ,
requiresLocation : challengeItem . challenge . data . metadata . requireLocation ,
challenge : challengeItem . challenge . data . challenge ,
signingPayload : challengeItem . signingPayload ,
deviceSummaryText : challengeItem . challenge . data . metadata . deviceLabel ,
locationSummaryText : locationSummary ( for : challengeItem . challenge ) ,
networkSummaryText : challengeItem . challenge . data . metadata . originHost ,
ipSummaryText : nil ,
expiresAtDate : Date ( millisecondsSince1970 : challengeItem . challenge . data . expiresAt ) ,
status : . pending
)
} ,
notifications : response . alerts . map { alert in
AppNotification (
serverID : alert . id ,
hintID : alert . data . notification . hintId ,
title : alert . data . title ,
message : alert . data . body ,
sentAt : Date ( millisecondsSince1970 : alert . data . createdAt ) ,
kind : notificationKind ( for : alert . data . category ) ,
isUnread : alert . data . seenAt = = nil
)
} ,
devices : response . devices . map { device in
PassportDeviceRecord (
id : device . id ,
label : device . data . label ,
platform : device . data . platform ,
lastSeenAt : device . data . lastSeenAt . map ( Date . init ( millisecondsSince1970 : ) ) ,
isCurrent : device . id = = session . passportDeviceID
)
}
)
}
private func validateSignedGPSPosition ( in request : PairingAuthenticationRequest ) throws {
if request . transport = = . nfc ,
request . signedGPSPosition = = nil {
throw AppError . missingSignedGPSPosition
}
if let signedGPSPosition = request . signedGPSPosition ,
! signedGPSPosition . verified ( for : request . pairingPayload ) {
throw AppError . invalidSignedGPSPosition
}
}
private func serverURL ( from context : PairingPayloadContext ) throws -> URL {
guard let url = URL ( string : " https:// \( context . originHost ) " ) else {
throw AppError . invalidPairingPayload
}
return url
}
private func buildEnrollmentSigningPayload ( from context : PairingPayloadContext ) throws -> String {
guard let challenge = context . challenge ,
let challengeID = context . challengeID else {
throw AppError . invalidPairingPayload
}
return [
" purpose=passport-enrollment " ,
" origin= \( context . originHost ) " ,
" token= \( context . pairingToken ) " ,
" challenge= \( challenge ) " ,
" challenge_id= \( challengeID ) "
] . joined ( separator : " \n " )
}
private func loadOrCreateKeyMaterial ( for originHost : String ) throws -> StoredPassportKeyMaterial {
if let existing = try keyStore . load ( key : originHost ) {
return existing
}
let privateKey = P256 . Signing . PrivateKey ( )
let material = StoredPassportKeyMaterial (
privateKeyBase64 : privateKey . rawRepresentation . base64EncodedString ( ) ,
publicKeyBase64 : privateKey . publicKey . x963Representation . base64EncodedString ( )
)
try keyStore . save ( material , key : originHost )
return material
}
private func privateKeyData ( for session : AuthSession ) throws -> Data {
guard let stored = try keyStore . load ( key : session . originHost ) ,
let data = Data ( base64Encoded : stored . privateKeyBase64 ) else {
throw AppError . invalidPairingPayload
}
return data
}
private func signPayload ( _ payload : String , with privateKeyData : Data ) throws -> String {
let privateKey = try P256 . Signing . PrivateKey ( rawRepresentation : privateKeyData )
let signature = try privateKey . signature ( for : Data ( payload . utf8 ) )
return signature . derRepresentation . base64EncodedString ( )
}
private func signedDeviceRequest (
session : AuthSession ,
action : String ,
signedFields : [ String ]
) throws -> SignedDeviceRequest {
let nonce = UUID ( ) . uuidString . lowercased ( )
let timestamp = Int ( Date ( ) . timeIntervalSince1970 * 1000 )
let payload = ( [
" purpose=passport-device-request " ,
" origin= \( session . originHost ) " ,
" action= \( action ) " ,
" device_id= \( session . passportDeviceID ) " ,
" timestamp= \( timestamp ) " ,
" nonce= \( nonce ) "
] + signedFields ) . joined ( separator : " \n " )
return SignedDeviceRequest (
deviceId : session . passportDeviceID ,
timestamp : timestamp ,
nonce : nonce ,
signatureBase64 : try signPayload ( payload , with : try privateKeyData ( for : session ) ) ,
signatureFormat : " der "
)
}
private func approvalKind ( for challenge : ServerPassportChallenge ) -> ApprovalRequestKind {
switch challenge . data . type {
case " authentication " :
return . signIn
case " physical_access " :
return . accessGrant
default :
return . elevatedAction
}
}
private func approvalRisk ( for challenge : ServerPassportChallenge ) -> ApprovalRisk {
if challenge . data . metadata . requireLocation ||
challenge . data . metadata . requireNfc ||
challenge . data . metadata . locationPolicy != nil ||
challenge . data . type != " authentication " {
return . elevated
}
return . routine
}
private func approvalTitle ( for challenge : ServerPassportChallenge ) -> String {
challenge . data . metadata . notificationTitle ? ? challenge . data . type . replacingOccurrences ( of : " _ " , with : " " ) . capitalized
}
private func approvalSubtitle ( for challenge : ServerPassportChallenge ) -> String {
if let audience = challenge . data . metadata . audience {
return " Approve this proof for \( audience ) . "
}
return " Approve this passport challenge on your trusted device. "
}
private func approvalScopes ( for challenge : ServerPassportChallenge ) -> [ String ] {
var scopes = [ " passport:device-proof " ]
if challenge . data . metadata . requireLocation {
scopes . append ( " context:location " )
}
if challenge . data . metadata . requireNfc {
scopes . append ( " context:nfc " )
}
if let label = challenge . data . metadata . locationPolicy ? . label {
scopes . append ( " policy: \( label ) " )
}
return scopes
}
private func locationSummary ( for challenge : ServerPassportChallenge ) -> String {
if let locationPolicy = challenge . data . metadata . locationPolicy {
return locationPolicy . label ? ? " Location within required area "
}
if challenge . data . metadata . requireLocation {
return " Current location required "
}
return " Location not required "
}
private func notificationKind ( for category : String ) -> AppNotificationKind {
switch category {
case " security " :
return . security
case " admin " :
return . approval
default :
return . system
}
}
}
2026-04-17 22:08:27 +02:00
actor MockIDPService : IDPServicing {
2026-04-19 16:29:13 +02:00
static let shared = MockIDPService ( )
2026-04-17 22:08:27 +02:00
private let profile = MemberProfile (
name : " Phil Kunz " ,
handle : " phil@idp.global " ,
organization : " idp.global " ,
deviceCount : 4 ,
recoverySummary : " Recovery kit healthy with 2 of 3 backup paths verified. "
)
2026-04-19 16:29:13 +02:00
private let appStateStore : AppStateStoring
2026-04-17 22:08:27 +02:00
private var requests : [ ApprovalRequest ] = [ ]
private var notifications : [ AppNotification ] = [ ]
2026-04-20 13:21:39 +00:00
private var devices : [ PassportDeviceRecord ] = [ ]
2026-04-17 22:08:27 +02:00
2026-04-19 16:29:13 +02:00
init ( appStateStore : AppStateStoring = UserDefaultsAppStateStore ( ) ) {
self . appStateStore = appStateStore
if let state = appStateStore . load ( ) {
requests = state . requests . sorted { $0 . createdAt > $1 . createdAt }
notifications = state . notifications . sorted { $0 . sentAt > $1 . sentAt }
2026-04-20 13:21:39 +00:00
devices = state . devices
2026-04-19 16:29:13 +02:00
} else {
requests = Self . seedRequests ( )
notifications = Self . seedNotifications ( )
2026-04-20 13:21:39 +00:00
devices = Self . seedDevices ( )
2026-04-19 16:29:13 +02:00
}
2026-04-17 22:08:27 +02:00
}
func bootstrap ( ) async throws -> BootstrapContext {
2026-04-19 16:29:13 +02:00
restoreSharedState ( )
2026-04-17 22:08:27 +02:00
try await Task . sleep ( for : . milliseconds ( 120 ) )
return BootstrapContext (
2026-04-18 01:05:22 +02:00
suggestedPairingPayload : " idp.global://pair?token=swiftapp-demo-berlin&origin=code.foss.global&device=Safari%20on%20Berlin%20MBP "
2026-04-17 22:08:27 +02:00
)
}
2026-04-18 01:05:22 +02:00
func signIn ( with request : PairingAuthenticationRequest ) async throws -> SignInResult {
2026-04-19 16:29:13 +02:00
restoreSharedState ( )
2026-04-17 22:08:27 +02:00
try await Task . sleep ( for : . milliseconds ( 260 ) )
2026-04-18 01:05:22 +02:00
try validateSignedGPSPosition ( in : request )
let session = try parseSession ( from : request )
2026-04-17 22:08:27 +02:00
notifications . insert (
AppNotification (
2026-04-18 01:05:22 +02:00
title : " Passport activated " ,
message : pairingMessage ( for : session ) ,
2026-04-17 22:08:27 +02:00
sentAt : . now ,
kind : . security ,
isUnread : true
) ,
at : 0
)
2026-04-19 16:29:13 +02:00
persistSharedStateIfAvailable ( )
2026-04-17 22:08:27 +02:00
return SignInResult (
session : session ,
snapshot : snapshot ( )
)
}
2026-04-18 01:05:22 +02:00
func identify ( with request : PairingAuthenticationRequest ) async throws -> DashboardSnapshot {
2026-04-19 16:29:13 +02:00
restoreSharedState ( )
2026-04-18 01:05:22 +02:00
try await Task . sleep ( for : . milliseconds ( 180 ) )
try validateSignedGPSPosition ( in : request )
2026-04-18 12:29:32 +02:00
let context = try PairingPayloadParser . parse ( request . pairingPayload )
2026-04-18 01:05:22 +02:00
notifications . insert (
AppNotification (
title : " Identity proof completed " ,
message : identificationMessage ( for : context , signedGPSPosition : request . signedGPSPosition ) ,
sentAt : . now ,
kind : . security ,
isUnread : true
) ,
at : 0
)
2026-04-19 16:29:13 +02:00
persistSharedStateIfAvailable ( )
2026-04-18 01:05:22 +02:00
return snapshot ( )
}
2026-04-17 22:08:27 +02:00
func refreshDashboard ( ) async throws -> DashboardSnapshot {
2026-04-19 16:29:13 +02:00
restoreSharedState ( )
2026-04-17 22:08:27 +02:00
try await Task . sleep ( for : . milliseconds ( 180 ) )
return snapshot ( )
}
func approveRequest ( id : UUID ) async throws -> DashboardSnapshot {
2026-04-19 16:29:13 +02:00
restoreSharedState ( )
2026-04-17 22:08:27 +02:00
try await Task . sleep ( for : . milliseconds ( 150 ) )
guard let index = requests . firstIndex ( where : { $0 . id = = id } ) else {
throw AppError . requestNotFound
}
requests [ index ] . status = . approved
notifications . insert (
AppNotification (
2026-04-18 01:05:22 +02:00
title : " Identity verified " ,
message : " \( requests [ index ] . title ) was completed for \( requests [ index ] . source ) . " ,
2026-04-17 22:08:27 +02:00
sentAt : . now ,
kind : . approval ,
isUnread : true
) ,
at : 0
)
2026-04-19 16:29:13 +02:00
persistSharedStateIfAvailable ( )
2026-04-17 22:08:27 +02:00
return snapshot ( )
}
func rejectRequest ( id : UUID ) async throws -> DashboardSnapshot {
2026-04-19 16:29:13 +02:00
restoreSharedState ( )
2026-04-17 22:08:27 +02:00
try await Task . sleep ( for : . milliseconds ( 150 ) )
guard let index = requests . firstIndex ( where : { $0 . id = = id } ) else {
throw AppError . requestNotFound
}
requests [ index ] . status = . rejected
notifications . insert (
AppNotification (
2026-04-18 01:05:22 +02:00
title : " Identity proof declined " ,
message : " \( requests [ index ] . title ) was declined before the session could continue. " ,
2026-04-17 22:08:27 +02:00
sentAt : . now ,
kind : . security ,
isUnread : true
) ,
at : 0
)
2026-04-19 16:29:13 +02:00
persistSharedStateIfAvailable ( )
2026-04-17 22:08:27 +02:00
return snapshot ( )
}
func simulateIncomingRequest ( ) async throws -> DashboardSnapshot {
2026-04-19 16:29:13 +02:00
restoreSharedState ( )
2026-04-17 22:08:27 +02:00
try await Task . sleep ( for : . milliseconds ( 120 ) )
let syntheticRequest = ApprovalRequest (
2026-04-18 01:05:22 +02:00
title : " Prove identity for web sign-in " ,
subtitle : " A browser session is asking this passport to prove that it is really you. " ,
source : " auth.idp.global " ,
2026-04-17 22:08:27 +02:00
createdAt : . now ,
2026-04-18 01:05:22 +02:00
kind : . signIn ,
risk : . routine ,
scopes : [ " proof:basic " , " client:web " , " method:qr " ] ,
2026-04-17 22:08:27 +02:00
status : . pending
)
requests . insert ( syntheticRequest , at : 0 )
notifications . insert (
AppNotification (
2026-04-18 01:05:22 +02:00
title : " Fresh identity proof request " ,
message : " A new relying party is waiting for your identity proof. " ,
2026-04-17 22:08:27 +02:00
sentAt : . now ,
kind : . approval ,
isUnread : true
) ,
at : 0
)
2026-04-19 16:29:13 +02:00
persistSharedStateIfAvailable ( )
2026-04-17 22:08:27 +02:00
return snapshot ( )
}
func markNotificationRead ( id : UUID ) async throws -> DashboardSnapshot {
2026-04-19 16:29:13 +02:00
restoreSharedState ( )
2026-04-17 22:08:27 +02:00
try await Task . sleep ( for : . milliseconds ( 80 ) )
guard let index = notifications . firstIndex ( where : { $0 . id = = id } ) else {
return snapshot ( )
}
notifications [ index ] . isUnread = false
2026-04-19 16:29:13 +02:00
persistSharedStateIfAvailable ( )
2026-04-17 22:08:27 +02:00
return snapshot ( )
}
private func snapshot ( ) -> DashboardSnapshot {
DashboardSnapshot (
profile : profile ,
requests : requests ,
2026-04-20 13:21:39 +00:00
notifications : notifications ,
devices : devices
2026-04-17 22:08:27 +02:00
)
}
2026-04-18 01:05:22 +02:00
private func validateSignedGPSPosition ( in request : PairingAuthenticationRequest ) throws {
if request . transport = = . nfc ,
request . signedGPSPosition = = nil {
throw AppError . missingSignedGPSPosition
}
if let signedGPSPosition = request . signedGPSPosition ,
! signedGPSPosition . verified ( for : request . pairingPayload ) {
throw AppError . invalidSignedGPSPosition
}
}
private func parseSession ( from request : PairingAuthenticationRequest ) throws -> AuthSession {
2026-04-18 12:29:32 +02:00
let context = try PairingPayloadParser . parse ( request . pairingPayload )
2026-04-18 01:05:22 +02:00
return AuthSession (
deviceName : context . deviceName ,
originHost : context . originHost ,
2026-04-20 13:21:39 +00:00
serverURL : " https:// \( context . originHost ) " ,
passportDeviceID : UUID ( ) . uuidString ,
2026-04-18 01:05:22 +02:00
pairedAt : . now ,
tokenPreview : context . tokenPreview ,
pairingCode : request . pairingPayload ,
pairingTransport : request . transport ,
signedGPSPosition : request . signedGPSPosition
)
}
private func pairingMessage ( for session : AuthSession ) -> String {
let transportSummary : String
switch session . pairingTransport {
case . qr :
transportSummary = " activated via QR "
case . nfc :
transportSummary = " activated via NFC with a signed GPS position "
case . manual :
transportSummary = " activated via manual payload "
case . preview :
transportSummary = " activated via preview payload "
}
if let signedGPSPosition = session . signedGPSPosition {
return " \( session . deviceName ) is now acting as a passport, \( transportSummary ) against \( session . originHost ) from \( signedGPSPosition . coordinateSummary ) \( signedGPSPosition . accuracySummary ) . "
}
return " \( session . deviceName ) is now acting as a passport, \( transportSummary ) against \( session . originHost ) . "
}
2026-04-18 12:29:32 +02:00
private func identificationMessage ( for context : PairingPayloadContext , signedGPSPosition : SignedGPSPosition ? ) -> String {
2026-04-18 01:05:22 +02:00
if let signedGPSPosition {
return " A signed GPS proof was sent for \( context . deviceName ) on \( context . originHost ) from \( signedGPSPosition . coordinateSummary ) \( signedGPSPosition . accuracySummary ) . "
}
return " An identity proof was completed for \( context . deviceName ) on \( context . originHost ) . "
}
2026-04-19 16:29:13 +02:00
private func restoreSharedState ( ) {
guard let state = appStateStore . load ( ) else {
requests = Self . seedRequests ( )
notifications = Self . seedNotifications ( )
2026-04-20 13:21:39 +00:00
devices = Self . seedDevices ( )
2026-04-19 16:29:13 +02:00
return
}
requests = state . requests . sorted { $0 . createdAt > $1 . createdAt }
notifications = state . notifications . sorted { $0 . sentAt > $1 . sentAt }
2026-04-20 13:21:39 +00:00
devices = state . devices
2026-04-19 16:29:13 +02:00
}
private func persistSharedStateIfAvailable ( ) {
guard let state = appStateStore . load ( ) else { return }
appStateStore . save (
PersistedAppState (
session : state . session ,
profile : state . profile ,
requests : requests ,
2026-04-20 13:21:39 +00:00
notifications : notifications ,
devices : devices
2026-04-19 16:29:13 +02:00
)
)
}
2026-04-17 22:08:27 +02:00
private static func seedRequests ( ) -> [ ApprovalRequest ] {
[
ApprovalRequest (
2026-04-18 01:05:22 +02:00
title : " Prove identity for Safari sign-in " ,
subtitle : " The portal wants this passport to prove that the browser session is really you. " ,
2026-04-17 22:08:27 +02:00
source : " code.foss.global " ,
createdAt : . now . addingTimeInterval ( - 60 * 12 ) ,
kind : . signIn ,
risk : . routine ,
2026-04-18 01:05:22 +02:00
scopes : [ " proof:basic " , " client:web " , " origin:trusted " ] ,
2026-04-17 22:08:27 +02:00
status : . pending
) ,
ApprovalRequest (
2026-04-18 01:05:22 +02:00
title : " Prove identity for workstation unlock " ,
subtitle : " Your secure workspace is asking for a stronger proof before it unlocks. " ,
source : " berlin-mbp.idp.global " ,
2026-04-17 22:08:27 +02:00
createdAt : . now . addingTimeInterval ( - 60 * 42 ) ,
2026-04-18 01:05:22 +02:00
kind : . elevatedAction ,
2026-04-17 22:08:27 +02:00
risk : . elevated ,
2026-04-18 01:05:22 +02:00
scopes : [ " proof:high " , " client:desktop " , " presence:required " ] ,
2026-04-17 22:08:27 +02:00
status : . pending
) ,
ApprovalRequest (
2026-04-18 01:05:22 +02:00
title : " Prove identity for CLI session " ,
subtitle : " The CLI session asked for proof earlier and was completed from this passport. " ,
2026-04-17 22:08:27 +02:00
source : " cli.idp.global " ,
createdAt : . now . addingTimeInterval ( - 60 * 180 ) ,
kind : . signIn ,
risk : . routine ,
2026-04-18 01:05:22 +02:00
scopes : [ " proof:basic " , " client:cli " ] ,
2026-04-17 22:08:27 +02:00
status : . approved
)
]
}
private static func seedNotifications ( ) -> [ AppNotification ] {
[
AppNotification (
2026-04-18 01:05:22 +02:00
title : " Two identity checks are waiting " ,
message : " One routine web proof and one stronger workstation proof are waiting for this passport. " ,
2026-04-17 22:08:27 +02:00
sentAt : . now . addingTimeInterval ( - 60 * 8 ) ,
kind : . approval ,
isUnread : true
) ,
AppNotification (
title : " Recovery health check passed " ,
message : " Backup recovery channels were verified in the last 24 hours. " ,
sentAt : . now . addingTimeInterval ( - 60 * 95 ) ,
kind : . system ,
isUnread : false
) ,
AppNotification (
2026-04-18 01:05:22 +02:00
title : " Passport quiet hours active " ,
message : " Routine identity checks will be delivered silently until the morning. " ,
2026-04-17 22:08:27 +02:00
sentAt : . now . addingTimeInterval ( - 60 * 220 ) ,
kind : . security ,
isUnread : false
)
]
}
2026-04-20 13:21:39 +00:00
private static func seedDevices ( ) -> [ PassportDeviceRecord ] {
[
PassportDeviceRecord ( id : UUID ( ) . uuidString , label : " Phil's iPhone " , platform : " ios " , lastSeenAt : . now , isCurrent : true ) ,
PassportDeviceRecord ( id : UUID ( ) . uuidString , label : " Phil's iPad Pro " , platform : " ipados " , lastSeenAt : . now . addingTimeInterval ( - 60 * 18 ) , isCurrent : false ) ,
PassportDeviceRecord ( id : UUID ( ) . uuidString , label : " Berlin MacBook Pro " , platform : " macos " , lastSeenAt : . now . addingTimeInterval ( - 60 * 74 ) , isCurrent : false )
]
}
}
private struct StoredPassportKeyMaterial : Codable , Equatable {
let privateKeyBase64 : String
let publicKeyBase64 : String
}
private protocol PassportKeyStoring {
func load ( key : String ) throws -> StoredPassportKeyMaterial ?
func save ( _ value : StoredPassportKeyMaterial , key : String ) throws
}
private final class DefaultPassportKeyStore : PassportKeyStoring {
private let service = " global.idp.swiftapp.passport-keys "
private let fallbackDefaults = UserDefaults . standard
func load ( key : String ) throws -> StoredPassportKeyMaterial ? {
#if canImport ( Security )
let query : [ String : Any ] = [
kSecClass as String : kSecClassGenericPassword ,
kSecAttrService as String : service ,
kSecAttrAccount as String : key ,
kSecReturnData as String : true ,
kSecMatchLimit as String : kSecMatchLimitOne
]
var result : CFTypeRef ?
let status = SecItemCopyMatching ( query as CFDictionary , & result )
if status = = errSecSuccess ,
let data = result as ? Data {
return try JSONDecoder ( ) . decode ( StoredPassportKeyMaterial . self , from : data )
}
if status = = errSecItemNotFound {
return nil
}
#endif
guard let data = fallbackDefaults . data ( forKey : " passport-key- \( key ) " ) else {
return nil
}
return try JSONDecoder ( ) . decode ( StoredPassportKeyMaterial . self , from : data )
}
func save ( _ value : StoredPassportKeyMaterial , key : String ) throws {
let data = try JSONEncoder ( ) . encode ( value )
#if canImport ( Security )
let query : [ String : Any ] = [
kSecClass as String : kSecClassGenericPassword ,
kSecAttrService as String : service ,
kSecAttrAccount as String : key
]
SecItemDelete ( query as CFDictionary )
let attributes : [ String : Any ] = [
kSecClass as String : kSecClassGenericPassword ,
kSecAttrService as String : service ,
kSecAttrAccount as String : key ,
kSecValueData as String : data
]
SecItemAdd ( attributes as CFDictionary , nil )
#endif
fallbackDefaults . set ( data , forKey : " passport-key- \( key ) " )
}
}
private enum DeviceEnvironment {
static var currentPlatform : String {
#if os ( iOS )
return " ios "
#elseif os ( watchOS )
return " watchos "
#elseif os ( macOS )
return " macos "
#else
return " unknown "
#endif
}
static var currentDeviceLabel : String {
#if canImport ( UIKit )
return UIDevice . current . name
#elseif os ( macOS )
return Host . current ( ) . localizedName ? ? " Mac Passport "
#elseif os ( watchOS )
return " Apple Watch Passport "
#else
return " Swift Passport Device "
#endif
}
static var capabilities : CapabilityRequest {
CapabilityRequest (
gps : true ,
nfc : {
#if os ( iOS )
return true
#else
return false
#endif
} ( ) ,
push : false
)
}
}
private struct LiveTypedRequestClient {
let endpoint : URL
private let session : URLSession = . shared
private let encoder = JSONEncoder ( )
private let decoder = JSONDecoder ( )
init ( baseURL : URL ) {
self . endpoint = baseURL . appending ( path : " typedrequest " )
}
func fire < Request : Encodable , Response : Decodable > (
method : String ,
request : Request ,
responseType : Response . Type
) async throws -> Response {
let envelope = TypedRequestEnvelope ( method : method , request : request , response : nil , correlation : TypedCorrelation ( phase : " request " ) )
return try await send ( envelope : envelope , method : method , responseType : responseType )
}
private func send < Request : Encodable , Response : Decodable > (
envelope : TypedRequestEnvelope < Request > ,
method : String ,
responseType : Response . Type
) async throws -> Response {
var urlRequest = URLRequest ( url : endpoint )
urlRequest . httpMethod = " POST "
urlRequest . httpBody = try encoder . encode ( envelope )
urlRequest . setValue ( " application/json " , forHTTPHeaderField : " Content-Type " )
urlRequest . setValue ( " application/json " , forHTTPHeaderField : " Accept " )
let ( data , response ) = try await session . data ( for : urlRequest )
guard let httpResponse = response as ? HTTPURLResponse ,
200 . . < 300 ~= httpResponse . statusCode else {
throw TypedRequestTransportError ( message : " Typed request transport failed " )
}
let typedResponse = try decoder . decode ( TypedResponseEnvelope < Response > . self , from : data )
if let error = typedResponse . error {
throw TypedRequestTransportError ( message : error . text )
}
if let retry = typedResponse . retry {
try await Task . sleep ( for : . milliseconds ( retry . waitForMs ) )
return try await send ( envelope : envelope , method : method , responseType : responseType )
}
guard let responsePayload = typedResponse . response else {
throw TypedRequestTransportError ( message : " Typed request returned no response payload " )
}
return responsePayload
}
}
private struct TypedCorrelation : Codable {
let id : String
let phase : String
init ( id : String = UUID ( ) . uuidString , phase : String ) {
self . id = id
self . phase = phase
}
}
private struct TypedRequestEnvelope < Request : Encodable > : Encodable {
let method : String
let request : Request
let response : EmptyJSONValue ?
let correlation : TypedCorrelation
}
private struct TypedResponseEnvelope < Response : Decodable > : Decodable {
let response : Response ?
let error : TypedResponseErrorPayload ?
let retry : TypedRetryInstruction ?
}
private struct TypedResponseErrorPayload : Decodable {
let text : String
}
private struct TypedRetryInstruction : Decodable {
let waitForMs : Int
}
private struct EmptyJSONValue : Codable { }
private struct TypedRequestTransportError : LocalizedError {
let message : String
var errorDescription : String ? { message }
}
private struct SignedDeviceRequest : Encodable {
let deviceId : String
let timestamp : Int
let nonce : String
let signatureBase64 : String
let signatureFormat : String
}
private struct CapabilityRequest : Encodable {
let gps : Bool
let nfc : Bool
let push : Bool
}
private struct CompletePassportEnrollmentRequest : Encodable {
let pairingToken : String
let deviceLabel : String
let platform : String
let publicKeyX963Base64 : String
let signatureBase64 : String
let signatureFormat : String
let appVersion : String ?
let capabilities : CapabilityRequest
}
private struct ApprovePassportChallengeRequest : Encodable {
let challengeId : String
let deviceId : String
let signatureBase64 : String
let signatureFormat : String
let location : LocationEvidenceRequest ?
let nfc : NFCEvidenceRequest ?
}
private struct RejectPassportChallengeRequest : Encodable {
let deviceId : String
let timestamp : Int
let nonce : String
let signatureBase64 : String
let signatureFormat : String
let challengeId : String
}
private struct MarkPassportAlertSeenRequest : Encodable {
let deviceId : String
let timestamp : Int
let nonce : String
let signatureBase64 : String
let signatureFormat : String
let hintId : String
}
private struct LocationEvidenceRequest : Encodable {
let latitude : Double
let longitude : Double
let accuracyMeters : Double
let capturedAt : Int
}
private struct NFCEvidenceRequest : Encodable {
let tagId : String ?
let readerId : String ?
}
private struct SimpleSuccessResponse : Decodable {
let success : Bool
}
private struct CompletePassportEnrollmentResponse : Decodable {
let device : ServerPassportDevice
}
private struct ApprovePassportChallengeResponse : Decodable {
let success : Bool
}
private struct RejectPassportChallengeResponse : Decodable {
let success : Bool
}
private struct PassportDashboardResponse : Decodable {
struct Profile : Decodable {
let userId : String
let name : String
let handle : String
let organizations : [ Organization ]
let deviceCount : Int
let recoverySummary : String
}
struct Organization : Decodable {
let id : String
let name : String
}
let profile : Profile
let devices : [ ServerPassportDevice ]
let challenges : [ ServerChallengeItem ]
let alerts : [ ServerAlert ]
}
private struct ServerChallengeItem : Decodable {
let challenge : ServerPassportChallenge
let signingPayload : String
}
private struct ServerPassportDevice : Decodable {
struct DataPayload : Decodable {
let userId : String
let label : String
let platform : String
let status : String
let lastSeenAt : Int ?
}
let id : String
let data : DataPayload
}
private struct ServerPassportChallenge : Decodable {
struct DataPayload : Decodable {
struct MetadataPayload : Decodable {
struct LocationPolicyPayload : Decodable {
let mode : String
let label : String ?
let latitude : Double
let longitude : Double
let radiusMeters : Double
let maxAccuracyMeters : Double ?
}
let originHost : String ?
let audience : String ?
let notificationTitle : String ?
let deviceLabel : String ?
let requireLocation : Bool
let requireNfc : Bool
let locationPolicy : LocationPolicyPayload ?
}
struct NotificationPayload : Decodable {
let hintId : String
}
let challenge : String
let type : String
let status : String
let metadata : MetadataPayload
let notification : NotificationPayload ?
let createdAt : Int
let expiresAt : Int
}
let id : String
let data : DataPayload
}
private struct ServerAlert : Decodable {
struct DataPayload : Decodable {
struct NotificationPayload : Decodable {
let hintId : String
}
let category : String
let title : String
let body : String
let createdAt : Int
let seenAt : Int ?
let notification : NotificationPayload
}
let id : String
let data : DataPayload
}
2026-04-20 14:10:43 +00:00
#if canImport ( CoreLocation ) && os ( iOS )
2026-04-20 13:21:39 +00:00
@ MainActor
private final class CurrentLocationEvidenceProvider : NSObject , @ preconcurrency CLLocationManagerDelegate {
private var manager : CLLocationManager ?
private var authorizationContinuation : CheckedContinuation < CLAuthorizationStatus , Never > ?
private var locationContinuation : CheckedContinuation < CLLocation , Error > ?
static func currentLocationEvidenceIfAvailable ( ) async throws -> LocationEvidenceRequest {
let provider = CurrentLocationEvidenceProvider ( )
return try await provider . currentLocationEvidence ( )
}
private func currentLocationEvidence ( ) async throws -> LocationEvidenceRequest {
let manager = CLLocationManager ( )
manager . delegate = self
manager . desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self . manager = manager
switch manager . authorizationStatus {
case . authorizedAlways , . authorizedWhenInUse :
break
case . notDetermined :
let status = await requestAuthorization ( using : manager )
guard status = = . authorizedAlways || status = = . authorizedWhenInUse else {
throw AppError . locationPermissionDenied
}
case . denied , . restricted :
throw AppError . locationPermissionDenied
@ unknown default :
throw AppError . locationUnavailable
}
let location = try await requestLocation ( using : manager )
return LocationEvidenceRequest (
latitude : location . coordinate . latitude ,
longitude : location . coordinate . longitude ,
accuracyMeters : location . horizontalAccuracy ,
capturedAt : Int ( location . timestamp . timeIntervalSince1970 * 1000 )
)
}
private func requestAuthorization ( using manager : CLLocationManager ) async -> CLAuthorizationStatus {
manager . requestWhenInUseAuthorization ( )
return await withCheckedContinuation { continuation in
authorizationContinuation = continuation
}
}
private func requestLocation ( using manager : CLLocationManager ) async throws -> CLLocation {
try await withCheckedThrowingContinuation { continuation in
locationContinuation = continuation
manager . requestLocation ( )
}
}
func locationManagerDidChangeAuthorization ( _ manager : CLLocationManager ) {
guard let continuation = authorizationContinuation else { return }
let status = manager . authorizationStatus
guard status != . notDetermined else { return }
authorizationContinuation = nil
continuation . resume ( returning : status )
}
func locationManager ( _ manager : CLLocationManager , didUpdateLocations locations : [ CLLocation ] ) {
guard let continuation = locationContinuation ,
let location = locations . last else {
return
}
authorizationContinuation = nil
locationContinuation = nil
self . manager = nil
continuation . resume ( returning : location )
}
func locationManager ( _ manager : CLLocationManager , didFailWithError error : Error ) {
guard let continuation = locationContinuation else { return }
authorizationContinuation = nil
locationContinuation = nil
self . manager = nil
continuation . resume ( throwing : AppError . locationUnavailable )
}
}
#else
private enum CurrentLocationEvidenceProvider {
static func currentLocationEvidenceIfAvailable ( ) async throws -> LocationEvidenceRequest {
throw AppError . locationUnavailable
}
}
#endif
private extension Date {
init ( millisecondsSince1970 : Int ) {
self = Date ( timeIntervalSince1970 : TimeInterval ( millisecondsSince1970 ) / 1000 )
}
2026-04-17 22:08:27 +02:00
}