From 832d82304d734341b5f6559e0f9533c8d933570f Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Thu, 24 Feb 2022 22:35:29 -0800 Subject: [PATCH] Updates. --- .../Sources/SecretAgentKit/Agent.swift | 4 +-- .../SecretAgentKit/SigningWitness.swift | 3 +-- .../Documentation.docc/Documentation.md | 5 +++- .../SecretKit/Erasers/AnySecretStore.swift | 10 +++++-- .../PersistedAuthenticationContext.swift | 9 +++++++ .../Sources/SecretKit/Types/SecretStore.swift | 10 +++++-- .../Sources/SecretKit/Types/SignedData.swift | 20 -------------- .../SecureEnclaveStore.swift | 27 +++++++++++-------- .../SmartCardSecretKit/SmartCardStore.swift | 8 ++++-- Sources/SecretAgent/Notifier.swift | 8 +++--- .../Preview Content/PreviewStore.swift | 4 +-- 11 files changed, 60 insertions(+), 48 deletions(-) create mode 100644 Sources/Packages/Sources/SecretKit/Types/PersistedAuthenticationContext.swift delete mode 100644 Sources/Packages/Sources/SecretKit/Types/SignedData.swift diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index 15c3545..fecee61 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -113,7 +113,7 @@ extension Agent { let dataToSign = reader.readNextChunk() let signed = try store.sign(data: dataToSign, with: secret, for: provenance) - let derSignature = signed.data + let derSignature = signed let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)! @@ -154,7 +154,7 @@ extension Agent { signedData.append(writer.lengthAndData(of: sub)) if let witness = witness { - try witness.witness(accessTo: secret, from: store, by: provenance, requiredAuthentication: signed.requiredAuthentication) + try witness.witness(accessTo: secret, from: store, by: provenance) } Logger().debug("Agent signed request") diff --git a/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift b/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift index ff1ae33..b090bd3 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SigningWitness.swift @@ -17,7 +17,6 @@ public protocol SigningWitness { /// - secret: The `Secret` that will was used to sign the request. /// - store: The `Store` that signed the request.. /// - provenance: A `SigningRequestProvenance` object describing the origin of the request. - /// - requiredAuthentication: A boolean describing whether or not authentication was required for the request. - func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws + func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws } diff --git a/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md b/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md index 8555c39..3c608d2 100644 --- a/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md +++ b/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md @@ -27,5 +27,8 @@ SecretKit is a collection of protocols describing secrets and stores. ### Signing Process -- ``SignedData`` - ``SigningRequestProvenance`` + +### Authentication Persistence + +- ``PersistedAuthenticationContext`` diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift index 305ecd2..4a05975 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift @@ -9,7 +9,8 @@ public class AnySecretStore: SecretStore { private let _id: () -> UUID private let _name: () -> String private let _secrets: () -> [AnySecret] - private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> SignedData + private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data + private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext? private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void private var sink: AnyCancellable? @@ -21,6 +22,7 @@ public class AnySecretStore: SecretStore { _id = { secretStore.id } _secrets = { secretStore.secrets.map { AnySecret($0) } } _sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) } + _existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) } _persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) } sink = secretStore.objectWillChange.sink { _ in self.objectWillChange.send() @@ -43,10 +45,14 @@ public class AnySecretStore: SecretStore { return _secrets() } - public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> SignedData { + public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> Data { try _sign(data, secret, provenance) } + public func existingPersistedAuthenticationContext(secret: AnySecret) -> PersistedAuthenticationContext? { + _existingPersistedAuthenticationContext(secret) + } + public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws { try _persistAuthentication(secret, duration) } diff --git a/Sources/Packages/Sources/SecretKit/Types/PersistedAuthenticationContext.swift b/Sources/Packages/Sources/SecretKit/Types/PersistedAuthenticationContext.swift new file mode 100644 index 0000000..65ceaf8 --- /dev/null +++ b/Sources/Packages/Sources/SecretKit/Types/PersistedAuthenticationContext.swift @@ -0,0 +1,9 @@ +import Foundation + +/// Protocol describing a persisted authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time. +public protocol PersistedAuthenticationContext { + /// Whether the context remains valid. + var valid: Bool { get } + /// The date at which the authorization expires and the context becomes invalid. + var expiration: Date { get } +} diff --git a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift index 88a280a..2251f5e 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift @@ -20,8 +20,14 @@ public protocol SecretStore: ObservableObject, Identifiable { /// - data: The data to sign. /// - secret: The ``Secret`` to sign with. /// - provenance: A ``SigningRequestProvenance`` describing where the request came from. - /// - Returns: A ``SignedData`` object, containing the signature and metadata about the signature process. - func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData + /// - Returns: The signed data. + func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data + + /// Checks to see if there is currently a valid persisted authentication for a given secret. + /// - Parameters: + /// - secret: The ``Secret`` to check if there is a persisted authentication for. + /// - Returns: A persisted authentication context, if a valid one exists. + func existingPersistedAuthenticationContext(secret: SecretType) -> PersistedAuthenticationContext? /// Persists user authorization for access to a secret. /// - Parameters: diff --git a/Sources/Packages/Sources/SecretKit/Types/SignedData.swift b/Sources/Packages/Sources/SecretKit/Types/SignedData.swift deleted file mode 100644 index 1468867..0000000 --- a/Sources/Packages/Sources/SecretKit/Types/SignedData.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -/// Describes the output of a sign request. -public struct SignedData { - - /// The signed data. - public let data: Data - /// A boolean describing whether authentication was required during the signature process. - public let requiredAuthentication: Bool - - /// Initializes a new SignedData. - /// - Parameters: - /// - data: The signed data. - /// - requiredAuthentication: A boolean describing whether authentication was required during the signature process. - public init(data: Data, requiredAuthentication: Bool) { - self.data = data - self.requiredAuthentication = requiredAuthentication - } - -} diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index e14536b..814d4af 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -100,7 +100,7 @@ extension SecureEnclave { reloadSecrets() } - public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData { + public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data { let context: LAContext if let existing = persistedAuthenticationContexts[secret], existing.valid { context = existing.context @@ -131,16 +131,15 @@ extension SecureEnclave { let key = untypedSafe as! SecKey var signError: SecurityError? - let signingStartTime = Date() guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else { throw SigningError(error: signError) } - 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 signature as Data + } - return SignedData(data: signature as Data, requiredAuthentication: requiredAuthentication) + public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? { + guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil } + return persisted } public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) throws { @@ -294,7 +293,7 @@ extension SecureEnclave { extension SecureEnclave { /// A context describing a persisted authentication. - private struct PersistentAuthenticationContext { + private struct PersistentAuthenticationContext: PersistedAuthenticationContext { /// The Secret to persist authentication for. let secret: Secret @@ -302,7 +301,7 @@ extension SecureEnclave { let context: LAContext /// An expiration date for the context. /// - Note - Monotonic time instead of Date() to prevent people setting the clock back. - let expiration: UInt64 + let monotonicExpiration: UInt64 /// Initializes a context. /// - Parameters: @@ -313,12 +312,18 @@ extension SecureEnclave { 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) + self.monotonicExpiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds) } /// A boolean describing whether or not the context is still valid. var valid: Bool { - clock_gettime_nsec_np(CLOCK_MONOTONIC) < expiration + clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration + } + + var expiration: Date { + let remainingNanoseconds = monotonicExpiration - clock_gettime_nsec_np(CLOCK_MONOTONIC) + let remainingInSeconds = Measurement(value: Double(remainingNanoseconds), unit: UnitDuration.nanoseconds).converted(to: .seconds).value + return Date(timeIntervalSinceNow: remainingInSeconds) } } diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index 830401c..4f90983 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -44,7 +44,7 @@ extension SmartCard { fatalError("Keys must be deleted on the smart card.") } - public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData { + public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data { guard let tokenID = tokenID else { fatalError() } let context = LAContext() context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\"" @@ -79,7 +79,11 @@ extension SmartCard { guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else { throw SigningError(error: signError) } - return SignedData(data: signature as Data, requiredAuthentication: false) + return signature as Data + } + + public func existingPersistedAuthenticationContext(secret: SmartCard.Secret) -> PersistedAuthenticationContext? { + nil } public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws { diff --git a/Sources/SecretAgent/Notifier.swift b/Sources/SecretAgent/Notifier.swift index ffa8059..b93e3aa 100644 --- a/Sources/SecretAgent/Notifier.swift +++ b/Sources/SecretAgent/Notifier.swift @@ -57,7 +57,7 @@ class Notifier { notificationCenter.requestAuthorization(options: .alert) { _, _ in } } - func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) { + func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) { notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret notificationDelegate.pendingPersistableStores[store.id.description] = store let notificationCenter = UNUserNotificationCenter.current() @@ -69,7 +69,7 @@ class Notifier { if #available(macOS 12.0, *) { notificationContent.interruptionLevel = .timeSensitive } - if requiredAuthentication { + if secret.requiresAuthentication && store.existingPersistedAuthenticationContext(secret: secret) == nil { notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier } if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) { @@ -106,8 +106,8 @@ extension Notifier: SigningWitness { func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws { } - func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws { - notify(accessTo: secret, from: store, by: provenance, requiredAuthentication: requiredAuthentication) + func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws { + notify(accessTo: secret, from: store, by: provenance) } } diff --git a/Sources/Secretive/Preview Content/PreviewStore.swift b/Sources/Secretive/Preview Content/PreviewStore.swift index b68c677..85dc347 100644 --- a/Sources/Secretive/Preview Content/PreviewStore.swift +++ b/Sources/Secretive/Preview Content/PreviewStore.swift @@ -36,8 +36,8 @@ extension Preview { self.secrets.append(contentsOf: new) } - func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> SignedData { - return SignedData(data: data, requiredAuthentication: false) + func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data { + return data } func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {