mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-31 13:08:17 +00:00
feat(security): implement keychain permission priming
Adds a modal sheet to explain why keychain access is needed before prompting the user after an app update.
This commit is contained in:
@@ -195,6 +195,8 @@ final class AppSettings: ObservableObject {
|
|||||||
didSet { save() }
|
didSet { save() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var showKeychainPrimer = false
|
||||||
|
|
||||||
@Published var message = ""
|
@Published var message = ""
|
||||||
|
|
||||||
private let defaults: UserDefaults
|
private let defaults: UserDefaults
|
||||||
@@ -236,12 +238,30 @@ final class AppSettings: ObservableObject {
|
|||||||
isKeychainAccessGranted = granted
|
isKeychainAccessGranted = granted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
|
||||||
|
let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
|
||||||
|
let fullVersion = "\(currentVersion).\(currentBuild)"
|
||||||
|
let lastVersion = defaults.string(forKey: "Firelink.lastLaunchedVersion")
|
||||||
|
defaults.set(fullVersion, forKey: "Firelink.lastLaunchedVersion")
|
||||||
|
|
||||||
|
var needsPrimer = false
|
||||||
if granted {
|
if granted {
|
||||||
if let token = KeychainCredentialStore.extensionToken() {
|
if let lastVersion, lastVersion != fullVersion {
|
||||||
extensionPairingToken = token
|
needsPrimer = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if granted {
|
||||||
|
if needsPrimer {
|
||||||
|
showKeychainPrimer = true
|
||||||
|
extensionPairingToken = ""
|
||||||
} else {
|
} else {
|
||||||
extensionPairingToken = Self.generateSecureToken()
|
if let token = KeychainCredentialStore.extensionToken() {
|
||||||
// The didSet of extensionPairingToken will handle setting it in the keychain since isKeychainAccessGranted is true.
|
extensionPairingToken = token
|
||||||
|
} else {
|
||||||
|
extensionPairingToken = Self.generateSecureToken()
|
||||||
|
// The didSet of extensionPairingToken will handle setting it in the keychain since isKeychainAccessGranted is true.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
extensionPairingToken = ""
|
extensionPairingToken = ""
|
||||||
@@ -367,6 +387,19 @@ final class AppSettings: ObservableObject {
|
|||||||
isKeychainAccessGranted = false
|
isKeychainAccessGranted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveKeychainPrimer(grantAccess: Bool) {
|
||||||
|
showKeychainPrimer = false
|
||||||
|
if grantAccess {
|
||||||
|
if let token = KeychainCredentialStore.extensionToken() {
|
||||||
|
extensionPairingToken = token
|
||||||
|
} else {
|
||||||
|
extensionPairingToken = Self.generateSecureToken()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isKeychainAccessGranted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
let stored = StoredSettings(
|
let stored = StoredSettings(
|
||||||
appTheme: appTheme,
|
appTheme: appTheme,
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $settings.showKeychainPrimer) {
|
||||||
|
KeychainPrimerView()
|
||||||
|
.environmentObject(settings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -243,3 +247,55 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct KeychainPrimerView: View {
|
||||||
|
@EnvironmentObject private var settings: AppSettings
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
Image(systemName: "lock.shield")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 64, height: 64)
|
||||||
|
.foregroundStyle(settings.appTheme.theme.accent ?? Color.accentColor)
|
||||||
|
.padding(.top, 16)
|
||||||
|
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Text("Security Update")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text("Firelink has been updated. To keep your browser extension running smoothly and your site logins secure, please re-authorize access to your Mac's Keychain on the next prompt.")
|
||||||
|
.font(.body)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Button {
|
||||||
|
settings.resolveKeychainPrimer(grantAccess: true)
|
||||||
|
} label: {
|
||||||
|
Text("Grant Secure Access")
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(.large)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
settings.resolveKeychainPrimer(grantAccess: false)
|
||||||
|
} label: {
|
||||||
|
Text("Not Now")
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.controlSize(.large)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
.padding(.bottom, 16)
|
||||||
|
}
|
||||||
|
.padding(24)
|
||||||
|
.frame(width: 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user