Refocus app around identity proof flows
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
enum AppSection: String, CaseIterable, Identifiable, Hashable {
|
||||
@@ -58,17 +59,119 @@ enum NotificationPermissionState: String, CaseIterable, Identifiable {
|
||||
case .unknown:
|
||||
"The app has not asked for notification delivery yet."
|
||||
case .allowed:
|
||||
"Alerts can break through immediately when a request arrives."
|
||||
"Identity proof alerts can break through immediately when a check arrives."
|
||||
case .provisional:
|
||||
"Notifications can be delivered quietly until the user promotes them."
|
||||
"Identity proof alerts can be delivered quietly until the user promotes them."
|
||||
case .denied:
|
||||
"Approval events stay in-app until the user re-enables notifications."
|
||||
"Identity proof events stay in-app until the user re-enables notifications."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BootstrapContext {
|
||||
let suggestedQRCodePayload: String
|
||||
let suggestedPairingPayload: String
|
||||
}
|
||||
|
||||
enum PairingTransport: String, Hashable {
|
||||
case qr
|
||||
case nfc
|
||||
case manual
|
||||
case preview
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .qr:
|
||||
"QR"
|
||||
case .nfc:
|
||||
"NFC"
|
||||
case .manual:
|
||||
"Manual"
|
||||
case .preview:
|
||||
"Preview"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PairingAuthenticationRequest: Hashable {
|
||||
let pairingPayload: String
|
||||
let transport: PairingTransport
|
||||
let signedGPSPosition: SignedGPSPosition?
|
||||
}
|
||||
|
||||
struct SignedGPSPosition: Hashable {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let horizontalAccuracyMeters: Double
|
||||
let capturedAt: Date
|
||||
let signatureBase64: String
|
||||
let publicKeyBase64: String
|
||||
|
||||
init(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
horizontalAccuracyMeters: Double,
|
||||
capturedAt: Date,
|
||||
signatureBase64: String = "",
|
||||
publicKeyBase64: String = ""
|
||||
) {
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.horizontalAccuracyMeters = horizontalAccuracyMeters
|
||||
self.capturedAt = capturedAt
|
||||
self.signatureBase64 = signatureBase64
|
||||
self.publicKeyBase64 = publicKeyBase64
|
||||
}
|
||||
|
||||
var coordinateSummary: String {
|
||||
"\(Self.normalized(latitude, precision: 5)), \(Self.normalized(longitude, precision: 5))"
|
||||
}
|
||||
|
||||
var accuracySummary: String {
|
||||
"±\(Int(horizontalAccuracyMeters.rounded())) m"
|
||||
}
|
||||
|
||||
func signingPayload(for pairingPayload: String) -> Data {
|
||||
let lines = [
|
||||
"payload=\(pairingPayload)",
|
||||
"latitude=\(Self.normalized(latitude, precision: 6))",
|
||||
"longitude=\(Self.normalized(longitude, precision: 6))",
|
||||
"accuracy=\(Self.normalized(horizontalAccuracyMeters, precision: 2))",
|
||||
"captured_at=\(Self.timestampFormatter.string(from: capturedAt))"
|
||||
]
|
||||
return Data(lines.joined(separator: "\n").utf8)
|
||||
}
|
||||
|
||||
func verified(for pairingPayload: String) -> Bool {
|
||||
guard let signatureData = Data(base64Encoded: signatureBase64),
|
||||
let publicKeyData = Data(base64Encoded: publicKeyBase64),
|
||||
let publicKey = try? P256.Signing.PublicKey(x963Representation: publicKeyData),
|
||||
let signature = try? P256.Signing.ECDSASignature(derRepresentation: signatureData) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return publicKey.isValidSignature(signature, for: signingPayload(for: pairingPayload))
|
||||
}
|
||||
|
||||
func signed(signatureData: Data, publicKeyData: Data) -> SignedGPSPosition {
|
||||
SignedGPSPosition(
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
horizontalAccuracyMeters: horizontalAccuracyMeters,
|
||||
capturedAt: capturedAt,
|
||||
signatureBase64: signatureData.base64EncodedString(),
|
||||
publicKeyBase64: publicKeyData.base64EncodedString()
|
||||
)
|
||||
}
|
||||
|
||||
private static let timestampFormatter: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private static func normalized(_ value: Double, precision: Int) -> String {
|
||||
String(format: "%.\(precision)f", locale: Locale(identifier: "en_US_POSIX"), value)
|
||||
}
|
||||
}
|
||||
|
||||
struct DashboardSnapshot {
|
||||
@@ -114,6 +217,8 @@ struct AuthSession: Identifiable, Hashable {
|
||||
let pairedAt: Date
|
||||
let tokenPreview: String
|
||||
let pairingCode: String
|
||||
let pairingTransport: PairingTransport
|
||||
let signedGPSPosition: SignedGPSPosition?
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
@@ -121,7 +226,9 @@ struct AuthSession: Identifiable, Hashable {
|
||||
originHost: String,
|
||||
pairedAt: Date,
|
||||
tokenPreview: String,
|
||||
pairingCode: String
|
||||
pairingCode: String,
|
||||
pairingTransport: PairingTransport = .manual,
|
||||
signedGPSPosition: SignedGPSPosition? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.deviceName = deviceName
|
||||
@@ -129,6 +236,8 @@ struct AuthSession: Identifiable, Hashable {
|
||||
self.pairedAt = pairedAt
|
||||
self.tokenPreview = tokenPreview
|
||||
self.pairingCode = pairingCode
|
||||
self.pairingTransport = pairingTransport
|
||||
self.signedGPSPosition = signedGPSPosition
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,17 +248,17 @@ enum ApprovalRequestKind: String, CaseIterable, Hashable {
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .signIn: "Sign-In"
|
||||
case .accessGrant: "Access Grant"
|
||||
case .elevatedAction: "Elevated Action"
|
||||
case .signIn: "Identity Check"
|
||||
case .accessGrant: "Strong Proof"
|
||||
case .elevatedAction: "Sensitive Proof"
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .signIn: "qrcode.viewfinder"
|
||||
case .accessGrant: "key.fill"
|
||||
case .elevatedAction: "shield.lefthalf.filled"
|
||||
case .accessGrant: "person.badge.shield.checkmark.fill"
|
||||
case .elevatedAction: "shield.checkered"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,18 +277,18 @@ enum ApprovalRisk: String, Hashable {
|
||||
var summary: String {
|
||||
switch self {
|
||||
case .routine:
|
||||
"Routine access to profile or sign-in scopes."
|
||||
"A familiar identity proof for a normal sign-in or check."
|
||||
case .elevated:
|
||||
"Sensitive access that can sign, publish, or unlock privileged actions."
|
||||
"A higher-assurance identity proof for a sensitive check."
|
||||
}
|
||||
}
|
||||
|
||||
var guidance: String {
|
||||
switch self {
|
||||
case .routine:
|
||||
"Review the origin and scope list, then approve if the session matches the device you expect."
|
||||
"Review the origin and continue only if it matches the proof you started."
|
||||
case .elevated:
|
||||
"Treat this like a privileged operation. Verify the origin, the requested scopes, and whether the action is time-bound before approving."
|
||||
"Only continue if you initiated this proof and trust the origin asking for it."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,8 +301,8 @@ enum ApprovalStatus: String, Hashable {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .pending: "Pending"
|
||||
case .approved: "Approved"
|
||||
case .rejected: "Rejected"
|
||||
case .approved: "Verified"
|
||||
case .rejected: "Declined"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,34 +350,34 @@ struct ApprovalRequest: Identifiable, Hashable {
|
||||
|
||||
var scopeSummary: String {
|
||||
if scopes.isEmpty {
|
||||
return "No scopes listed"
|
||||
return "No proof details listed"
|
||||
}
|
||||
|
||||
let suffix = scopes.count == 1 ? "" : "s"
|
||||
return "\(scopes.count) requested scope\(suffix)"
|
||||
return "\(scopes.count) proof detail\(suffix)"
|
||||
}
|
||||
|
||||
var trustHeadline: String {
|
||||
switch (kind, risk) {
|
||||
case (.signIn, .routine):
|
||||
"Low-friction sign-in request"
|
||||
"Standard identity proof"
|
||||
case (.signIn, .elevated):
|
||||
"Privileged sign-in request"
|
||||
"High-assurance sign-in proof"
|
||||
case (.accessGrant, _):
|
||||
"Token grant request"
|
||||
"Cross-device identity proof"
|
||||
case (.elevatedAction, _):
|
||||
"Sensitive action request"
|
||||
"Sensitive identity proof"
|
||||
}
|
||||
}
|
||||
|
||||
var trustDetail: String {
|
||||
switch kind {
|
||||
case .signIn:
|
||||
"This request usually creates or refreshes a session token for a browser, CLI, or device."
|
||||
"This request proves that the person at the browser, CLI, or device is really you."
|
||||
case .accessGrant:
|
||||
"This request issues scoped access for a service or automation that wants to act on your behalf."
|
||||
"This request asks for a stronger proof so the relying party can trust the session with higher confidence."
|
||||
case .elevatedAction:
|
||||
"This request performs a privileged action such as signing, publishing, or creating short-lived credentials."
|
||||
"This request asks for the highest confidence proof before continuing with a sensitive flow."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,7 +389,7 @@ enum AppNotificationKind: String, Hashable {
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .approval: "Approval"
|
||||
case .approval: "Proof"
|
||||
case .security: "Security"
|
||||
case .system: "System"
|
||||
}
|
||||
@@ -297,9 +406,9 @@ enum AppNotificationKind: String, Hashable {
|
||||
var summary: String {
|
||||
switch self {
|
||||
case .approval:
|
||||
"Decision and approval activity"
|
||||
"Identity proof activity"
|
||||
case .security:
|
||||
"Pairing and security posture updates"
|
||||
"Passport and security posture updates"
|
||||
case .system:
|
||||
"Product and environment status messages"
|
||||
}
|
||||
@@ -332,15 +441,27 @@ struct AppNotification: Identifiable, Hashable {
|
||||
}
|
||||
|
||||
enum AppError: LocalizedError {
|
||||
case invalidQRCode
|
||||
case invalidPairingPayload
|
||||
case missingSignedGPSPosition
|
||||
case invalidSignedGPSPosition
|
||||
case locationPermissionDenied
|
||||
case locationUnavailable
|
||||
case requestNotFound
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidQRCode:
|
||||
"That QR payload is not valid for idp.global sign-in."
|
||||
case .invalidPairingPayload:
|
||||
"That idp.global payload is not valid for this action."
|
||||
case .missingSignedGPSPosition:
|
||||
"Tap NFC requires a signed GPS position."
|
||||
case .invalidSignedGPSPosition:
|
||||
"The signed GPS position attached to this NFC proof could not be verified."
|
||||
case .locationPermissionDenied:
|
||||
"Location access is required so Tap NFC can attach a signed GPS position."
|
||||
case .locationUnavailable:
|
||||
"Unable to determine the current GPS position for Tap NFC."
|
||||
case .requestNotFound:
|
||||
"The selected request could not be found."
|
||||
"The selected identity check could not be found."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user