mirror of
https://github.com/maxgoedjen/secretive.git
synced 2024-11-24 14:37:08 +00:00
Allow limited key validity window for authenticated keys (#252)
This commit is contained in:
parent
faed5514e2
commit
7d9fee0546
@ -14,8 +14,42 @@ class Notifier {
|
|||||||
let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: [])
|
let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: [])
|
||||||
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
|
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
|
||||||
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
|
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
|
||||||
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
|
|
||||||
|
let rawDurations = [
|
||||||
|
Measurement(value: 1, unit: UnitDuration.minutes),
|
||||||
|
Measurement(value: 5, unit: UnitDuration.minutes),
|
||||||
|
Measurement(value: 1, unit: UnitDuration.hours),
|
||||||
|
Measurement(value: 24, unit: UnitDuration.hours)
|
||||||
|
]
|
||||||
|
|
||||||
|
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: "Do Not Unlock", options: [])
|
||||||
|
var allPersistenceActions = [doNotPersistAction]
|
||||||
|
|
||||||
|
let formatter = DateComponentsFormatter()
|
||||||
|
formatter.unitsStyle = .spellOut
|
||||||
|
formatter.allowedUnits = [.hour, .minute, .day]
|
||||||
|
|
||||||
|
for duration in rawDurations {
|
||||||
|
let seconds = duration.converted(to: .seconds).value
|
||||||
|
guard let string = formatter.string(from: seconds)?.capitalized else { continue }
|
||||||
|
let identifier = Constants.persistAuthenticationCategoryIdentitifier.appending("\(seconds)")
|
||||||
|
let action = UNNotificationAction(identifier: identifier, title: string, options: [])
|
||||||
|
notificationDelegate.persistOptions[identifier] = seconds
|
||||||
|
allPersistenceActions.append(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
let persistAuthenticationCategory = UNNotificationCategory(identifier: Constants.persistAuthenticationCategoryIdentitifier, actions: allPersistenceActions, intentIdentifiers: [], options: [])
|
||||||
|
if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
|
||||||
|
persistAuthenticationCategory.setValue("Leave Unlocked", forKey: "_actionsMenuTitle")
|
||||||
|
}
|
||||||
|
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
|
||||||
UNUserNotificationCenter.current().delegate = notificationDelegate
|
UNUserNotificationCenter.current().delegate = notificationDelegate
|
||||||
|
|
||||||
|
notificationDelegate.persistAuthentication = { secret, store, duration in
|
||||||
|
guard let duration = duration else { return }
|
||||||
|
try? store.persistAuthentication(secret: secret, forDuration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prompt() {
|
func prompt() {
|
||||||
@ -23,11 +57,21 @@ class Notifier {
|
|||||||
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
|
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
func notify(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) {
|
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) {
|
||||||
|
notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret
|
||||||
|
notificationDelegate.pendingPersistableStores[store.id.description] = store
|
||||||
let notificationCenter = UNUserNotificationCenter.current()
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
let notificationContent = UNMutableNotificationContent()
|
let notificationContent = UNMutableNotificationContent()
|
||||||
notificationContent.title = "Signed Request from \(provenance.origin.displayName)"
|
notificationContent.title = "Signed Request from \(provenance.origin.displayName)"
|
||||||
notificationContent.subtitle = "Using secret \"\(secret.name)\""
|
notificationContent.subtitle = "Using secret \"\(secret.name)\""
|
||||||
|
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
|
||||||
|
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
|
||||||
|
if #available(macOS 12.0, *) {
|
||||||
|
notificationContent.interruptionLevel = .timeSensitive
|
||||||
|
}
|
||||||
|
if requiredAuthentication {
|
||||||
|
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
|
||||||
|
}
|
||||||
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
||||||
notificationContent.attachments = [attachment]
|
notificationContent.attachments = [attachment]
|
||||||
}
|
}
|
||||||
@ -41,6 +85,9 @@ class Notifier {
|
|||||||
let notificationCenter = UNUserNotificationCenter.current()
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
let notificationContent = UNMutableNotificationContent()
|
let notificationContent = UNMutableNotificationContent()
|
||||||
if update.critical {
|
if update.critical {
|
||||||
|
if #available(macOS 12.0, *) {
|
||||||
|
notificationContent.interruptionLevel = .critical
|
||||||
|
}
|
||||||
notificationContent.title = "Critical Security Update - \(update.name)"
|
notificationContent.title = "Critical Security Update - \(update.name)"
|
||||||
} else {
|
} else {
|
||||||
notificationContent.title = "Update Available - \(update.name)"
|
notificationContent.title = "Update Available - \(update.name)"
|
||||||
@ -56,11 +103,11 @@ class Notifier {
|
|||||||
|
|
||||||
extension Notifier: SigningWitness {
|
extension Notifier: SigningWitness {
|
||||||
|
|
||||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws {
|
||||||
notify(accessTo: secret, by: provenance)
|
notify(accessTo: secret, from: store, by: provenance, requiredAuthentication: requiredAuthentication)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -68,10 +115,20 @@ extension Notifier: SigningWitness {
|
|||||||
extension Notifier {
|
extension Notifier {
|
||||||
|
|
||||||
enum Constants {
|
enum Constants {
|
||||||
|
|
||||||
|
// Update notifications
|
||||||
static let updateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update"
|
static let updateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update"
|
||||||
static let criticalUpdateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.critical"
|
static let criticalUpdateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.critical"
|
||||||
static let updateActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.updateaction"
|
static let updateActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.updateaction"
|
||||||
static let ignoreActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.ignoreaction"
|
static let ignoreActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.ignoreaction"
|
||||||
|
|
||||||
|
// Authorization persistence notificatoins
|
||||||
|
static let persistAuthenticationCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication"
|
||||||
|
static let doNotPersistActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.donotpersist"
|
||||||
|
static let persistForActionIdentitifierPrefix = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.persist."
|
||||||
|
|
||||||
|
static let persistSecretIDKey = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.secretidkey"
|
||||||
|
static let persistStoreIDKey = "com.maxgoedjen.Secretive.SecretAgent.persistauthentication.storeidkey"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -80,12 +137,30 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
|
|
||||||
fileprivate var release: Release?
|
fileprivate var release: Release?
|
||||||
fileprivate var ignore: ((Release) -> Void)?
|
fileprivate var ignore: ((Release) -> Void)?
|
||||||
|
fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) -> Void)?
|
||||||
|
fileprivate var persistOptions: [String: TimeInterval] = [:]
|
||||||
|
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
|
||||||
|
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
|
||||||
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
|
let category = response.notification.request.content.categoryIdentifier
|
||||||
|
switch category {
|
||||||
|
case Notifier.Constants.updateCategoryIdentitifier:
|
||||||
|
handleUpdateResponse(response: response)
|
||||||
|
case Notifier.Constants.persistAuthenticationCategoryIdentitifier:
|
||||||
|
handlePersistAuthenticationResponse(response: response)
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUpdateResponse(response: UNNotificationResponse) {
|
||||||
guard let update = release else { return }
|
guard let update = release else { return }
|
||||||
switch response.actionIdentifier {
|
switch response.actionIdentifier {
|
||||||
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
|
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
|
||||||
@ -95,7 +170,14 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
default:
|
default:
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
completionHandler()
|
}
|
||||||
|
|
||||||
|
func handlePersistAuthenticationResponse(response: UNNotificationResponse) {
|
||||||
|
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = pendingPersistableSecrets[secretID],
|
||||||
|
let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = pendingPersistableStores[storeID]
|
||||||
|
else { return }
|
||||||
|
pendingPersistableSecrets[secretID] = nil
|
||||||
|
persistAuthentication?(secret, store, persistOptions[response.actionIdentifier])
|
||||||
}
|
}
|
||||||
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
@ -89,11 +89,12 @@ extension Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let witness = witness {
|
if let witness = witness {
|
||||||
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, by: provenance)
|
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataToSign = reader.readNextChunk()
|
let dataToSign = reader.readNextChunk()
|
||||||
let derSignature = try store.sign(data: dataToSign, with: secret, for: provenance)
|
let signed = try store.sign(data: dataToSign, with: secret, for: provenance)
|
||||||
|
let derSignature = signed.data
|
||||||
|
|
||||||
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||||
|
|
||||||
@ -134,7 +135,7 @@ extension Agent {
|
|||||||
signedData.append(writer.lengthAndData(of: sub))
|
signedData.append(writer.lengthAndData(of: sub))
|
||||||
|
|
||||||
if let witness = witness {
|
if let witness = witness {
|
||||||
try witness.witness(accessTo: secret, by: provenance)
|
try witness.witness(accessTo: secret, from: store, by: provenance, requiredAuthentication: signed.requiredAuthentication)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger().debug("Agent signed request")
|
Logger().debug("Agent signed request")
|
||||||
|
@ -3,7 +3,7 @@ import SecretKit
|
|||||||
|
|
||||||
public protocol SigningWitness {
|
public protocol SigningWitness {
|
||||||
|
|
||||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws
|
||||||
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ extension Stub {
|
|||||||
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))")
|
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> SignedData {
|
||||||
guard !shouldThrow else {
|
guard !shouldThrow else {
|
||||||
throw NSError(domain: "test", code: 0, userInfo: nil)
|
throw NSError(domain: "test", code: 0, userInfo: nil)
|
||||||
}
|
}
|
||||||
@ -67,7 +67,10 @@ extension Stub {
|
|||||||
default:
|
default:
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data
|
return SignedData(data: SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data, requiredAuthentication: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,14 +10,14 @@ struct StubWitness {
|
|||||||
|
|
||||||
extension StubWitness: SigningWitness {
|
extension StubWitness: SigningWitness {
|
||||||
|
|
||||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
|
||||||
let objection = speakNow(secret, provenance)
|
let objection = speakNow(secret, provenance)
|
||||||
if objection {
|
if objection {
|
||||||
throw TheresMyChance()
|
throw TheresMyChance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws {
|
||||||
witness(secret, provenance)
|
witness(secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,9 @@ public class AnySecretStore: SecretStore {
|
|||||||
private let _id: () -> UUID
|
private let _id: () -> UUID
|
||||||
private let _name: () -> String
|
private let _name: () -> String
|
||||||
private let _secrets: () -> [AnySecret]
|
private let _secrets: () -> [AnySecret]
|
||||||
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
|
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> SignedData
|
||||||
|
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
|
||||||
|
|
||||||
private var sink: AnyCancellable?
|
private var sink: AnyCancellable?
|
||||||
|
|
||||||
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
||||||
@ -18,6 +20,7 @@ public class AnySecretStore: SecretStore {
|
|||||||
_id = { secretStore.id }
|
_id = { secretStore.id }
|
||||||
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
||||||
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
||||||
|
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
||||||
sink = secretStore.objectWillChange.sink { _ in
|
sink = secretStore.objectWillChange.sink { _ in
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
@ -39,10 +42,14 @@ public class AnySecretStore: SecretStore {
|
|||||||
return _secrets()
|
return _secrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> Data {
|
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> SignedData {
|
||||||
try _sign(data, secret, provenance)
|
try _sign(data, secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws {
|
||||||
|
try _persistAuthentication(secret, duration)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||||
@ -69,4 +76,5 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
|||||||
public func update(secret: AnySecret, name: String) throws {
|
public func update(secret: AnySecret, name: String) throws {
|
||||||
try _update(secret, name)
|
try _update(secret, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@ public protocol SecretStore: ObservableObject, Identifiable {
|
|||||||
var name: String { get }
|
var name: String { get }
|
||||||
var secrets: [SecretType] { get }
|
var secrets: [SecretType] { get }
|
||||||
|
|
||||||
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data
|
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData
|
||||||
|
|
||||||
|
func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) throws
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
13
SecretKit/Common/Types/SignedData.swift
Normal file
13
SecretKit/Common/Types/SignedData.swift
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct SignedData {
|
||||||
|
|
||||||
|
public let data: Data
|
||||||
|
public let requiredAuthentication: Bool
|
||||||
|
|
||||||
|
public init(data: Data, requiredAuthentication: Bool) {
|
||||||
|
self.data = data
|
||||||
|
self.requiredAuthentication = requiredAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -17,6 +17,8 @@ extension SecureEnclave {
|
|||||||
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
|
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
|
||||||
@Published public private(set) var secrets: [Secret] = []
|
@Published public private(set) var secrets: [Secret] = []
|
||||||
|
|
||||||
|
private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
|
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
|
||||||
self.reloadSecrets(notify: false)
|
self.reloadSecrets(notify: false)
|
||||||
@ -93,10 +95,16 @@ extension SecureEnclave {
|
|||||||
reloadSecrets()
|
reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
|
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData {
|
||||||
let context = LAContext()
|
let context: LAContext
|
||||||
|
if let existing = persistedAuthenticationContexts[secret], existing.valid {
|
||||||
|
context = existing.context
|
||||||
|
} else {
|
||||||
|
let newContext = LAContext()
|
||||||
|
newContext.localizedCancelTitle = "Deny"
|
||||||
|
context = newContext
|
||||||
|
}
|
||||||
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
||||||
context.localizedCancelTitle = "Deny"
|
|
||||||
let attributes = [
|
let attributes = [
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
@ -117,10 +125,37 @@ extension SecureEnclave {
|
|||||||
}
|
}
|
||||||
let key = untypedSafe as! SecKey
|
let key = untypedSafe as! SecKey
|
||||||
var signError: SecurityError?
|
var signError: SecurityError?
|
||||||
|
|
||||||
|
let signingStartTime = Date()
|
||||||
guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else {
|
guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else {
|
||||||
throw SigningError(error: signError)
|
throw SigningError(error: signError)
|
||||||
}
|
}
|
||||||
return signature as Data
|
let signatureDuration = Date().timeIntervalSince(signingStartTime)
|
||||||
|
// Hack to determine if the user had to authenticate to sign.
|
||||||
|
// Since there's now way to inspect SecAccessControl to determine (afaict).
|
||||||
|
let requiredAuthentication = signatureDuration > Constants.unauthenticatedThreshold
|
||||||
|
|
||||||
|
return SignedData(data: signature as Data, requiredAuthentication: requiredAuthentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) throws {
|
||||||
|
let newContext = LAContext()
|
||||||
|
newContext.localizedCancelTitle = "Deny"
|
||||||
|
|
||||||
|
let formatter = DateComponentsFormatter()
|
||||||
|
formatter.unitsStyle = .spellOut
|
||||||
|
formatter.allowedUnits = [.hour, .minute, .day]
|
||||||
|
|
||||||
|
if let durationString = formatter.string(from: duration) {
|
||||||
|
newContext.localizedReason = "unlock secret \"\(secret.name)\" for \(durationString)"
|
||||||
|
} else {
|
||||||
|
newContext.localizedReason = "unlock secret \"\(secret.name)\""
|
||||||
|
}
|
||||||
|
newContext.evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) { [weak self] success, _ in
|
||||||
|
guard success else { return }
|
||||||
|
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
|
||||||
|
self?.persistedAuthenticationContexts[secret] = context
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -177,6 +212,7 @@ extension SecureEnclave.Store {
|
|||||||
throw SecureEnclave.KeychainError(statusCode: status)
|
throw SecureEnclave.KeychainError(statusCode: status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
@ -202,6 +238,30 @@ extension SecureEnclave {
|
|||||||
enum Constants {
|
enum Constants {
|
||||||
static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData
|
static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData
|
||||||
static let keyType = kSecAttrKeyTypeECSECPrimeRandom
|
static let keyType = kSecAttrKeyTypeECSECPrimeRandom
|
||||||
|
static let unauthenticatedThreshold: TimeInterval = 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecureEnclave {
|
||||||
|
|
||||||
|
private struct PersistentAuthenticationContext {
|
||||||
|
|
||||||
|
let secret: Secret
|
||||||
|
let context: LAContext
|
||||||
|
// Monotonic time instead of Date() to prevent people setting the clock back.
|
||||||
|
let expiration: UInt64
|
||||||
|
|
||||||
|
init(secret: Secret, context: LAContext, duration: TimeInterval) {
|
||||||
|
self.secret = secret
|
||||||
|
self.context = context
|
||||||
|
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
|
||||||
|
self.expiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
var valid: Bool {
|
||||||
|
clock_gettime_nsec_np(CLOCK_MONOTONIC) < expiration
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ extension SmartCard {
|
|||||||
fatalError("Keys must be deleted on the smart card.")
|
fatalError("Keys must be deleted on the smart card.")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
|
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData {
|
||||||
guard let tokenID = tokenID else { fatalError() }
|
guard let tokenID = tokenID else { fatalError() }
|
||||||
let context = LAContext()
|
let context = LAContext()
|
||||||
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
||||||
@ -78,7 +78,10 @@ extension SmartCard {
|
|||||||
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
|
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
|
||||||
throw SigningError(error: signError)
|
throw SigningError(error: signError)
|
||||||
}
|
}
|
||||||
return signature as Data
|
return SignedData(data: signature as Data, requiredAuthentication: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||||
501B7AE1251C56F700776EC7 /* SigningRequestProvenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */; };
|
501B7AE1251C56F700776EC7 /* SigningRequestProvenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */; };
|
||||||
|
5035FF6E2737A2F4006FE1F6 /* SignedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5035FF6D2737A2F4006FE1F6 /* SignedData.swift */; };
|
||||||
50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */; };
|
50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */; };
|
||||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
||||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
||||||
@ -228,6 +229,7 @@
|
|||||||
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
||||||
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||||
|
5035FF6D2737A2F4006FE1F6 /* SignedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedData.swift; sourceTree = "<group>"; };
|
||||||
50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSHWriterTests.swift; sourceTree = "<group>"; };
|
50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSHWriterTests.swift; sourceTree = "<group>"; };
|
||||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
|
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
|
||||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
|
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
|
||||||
@ -394,6 +396,7 @@
|
|||||||
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */,
|
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */,
|
||||||
50617DCA23FCECA10099B055 /* Secret.swift */,
|
50617DCA23FCECA10099B055 /* Secret.swift */,
|
||||||
50617DC623FCE4EA0099B055 /* SecretStore.swift */,
|
50617DC623FCE4EA0099B055 /* SecretStore.swift */,
|
||||||
|
5035FF6D2737A2F4006FE1F6 /* SignedData.swift */,
|
||||||
);
|
);
|
||||||
path = Types;
|
path = Types;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1059,6 +1062,7 @@
|
|||||||
5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */,
|
5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */,
|
||||||
5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */,
|
5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */,
|
||||||
50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */,
|
50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */,
|
||||||
|
5035FF6E2737A2F4006FE1F6 /* SignedData.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -35,8 +35,11 @@ extension Preview {
|
|||||||
self.secrets.append(contentsOf: new)
|
self.secrets.append(contentsOf: new)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> SignedData {
|
||||||
return data
|
return SignedData(data: data, requiredAuthentication: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user