mirror of
				https://github.com/maxgoedjen/secretive.git
				synced 2025-11-04 01:10:56 +00:00 
			
		
		
		
	Merge branch 'main' into asyncawait
This commit is contained in:
		
						commit
						9b1f33b332
					
				
							
								
								
									
										6
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@ -20,7 +20,7 @@ jobs:
 | 
			
		||||
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
 | 
			
		||||
      run: ./.github/scripts/signing.sh
 | 
			
		||||
    - name: Set Environment
 | 
			
		||||
      run: sudo xcrun xcode-select -s /Applications/Xcode_13.0.app
 | 
			
		||||
      run: sudo xcrun xcode-select -s /Applications/Xcode_13.1.app
 | 
			
		||||
    - name: Test
 | 
			
		||||
      run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
 | 
			
		||||
  build:
 | 
			
		||||
@ -38,7 +38,7 @@ jobs:
 | 
			
		||||
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
 | 
			
		||||
      run: ./.github/scripts/signing.sh
 | 
			
		||||
    - name: Set Environment
 | 
			
		||||
      run: sudo xcrun xcode-select -s /Applications/Xcode_13.0.app
 | 
			
		||||
      run: sudo xcrun xcode-select -s /Applications/Xcode_13.1.app
 | 
			
		||||
    - name: Update Build Number
 | 
			
		||||
      env:
 | 
			
		||||
        TAG_NAME: ${{ github.ref }}
 | 
			
		||||
@ -58,7 +58,7 @@ jobs:
 | 
			
		||||
      env: 
 | 
			
		||||
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
 | 
			
		||||
        APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
 | 
			
		||||
      run: xcrun altool --notarize-app --primary-bundle-id "com.maxgoedjen.secretive.host" --apiKey $APPLE_API_KEY_ID --apiIssuer $APPLE_API_ISSUER --file Secretive.zip
 | 
			
		||||
      run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
 | 
			
		||||
    - name: Document SHAs
 | 
			
		||||
      run: |
 | 
			
		||||
            shasum -a 512 Secretive.zip
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							@ -8,6 +8,6 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v2
 | 
			
		||||
    - name: Set Environment
 | 
			
		||||
      run: sudo xcrun xcode-select -s /Applications/Xcode_13.0.app
 | 
			
		||||
      run: sudo xcrun xcode-select -s /Applications/Xcode_13.1.app
 | 
			
		||||
    - name: Test
 | 
			
		||||
      run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
 | 
			
		||||
 | 
			
		||||
@ -14,8 +14,42 @@ class Notifier {
 | 
			
		||||
        let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: [])
 | 
			
		||||
        let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], 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
 | 
			
		||||
 | 
			
		||||
        notificationDelegate.persistAuthentication = { secret, store, duration in
 | 
			
		||||
            guard let duration = duration else { return }
 | 
			
		||||
            try? store.persistAuthentication(secret: secret, forDuration: duration)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func prompt() {
 | 
			
		||||
@ -23,11 +57,21 @@ class Notifier {
 | 
			
		||||
        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 notificationContent = UNMutableNotificationContent()
 | 
			
		||||
        notificationContent.title = "Signed Request from \(provenance.origin.displayName)"
 | 
			
		||||
        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) {
 | 
			
		||||
            notificationContent.attachments = [attachment]
 | 
			
		||||
        }
 | 
			
		||||
@ -41,6 +85,9 @@ class Notifier {
 | 
			
		||||
        let notificationCenter = UNUserNotificationCenter.current()
 | 
			
		||||
        let notificationContent = UNMutableNotificationContent()
 | 
			
		||||
        if update.critical {
 | 
			
		||||
            if #available(macOS 12.0, *) {
 | 
			
		||||
                notificationContent.interruptionLevel = .critical
 | 
			
		||||
            }
 | 
			
		||||
            notificationContent.title = "Critical Security Update - \(update.name)"
 | 
			
		||||
        } else {
 | 
			
		||||
            notificationContent.title = "Update Available - \(update.name)"
 | 
			
		||||
@ -56,11 +103,11 @@ class Notifier {
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
        notify(accessTo: secret, by: provenance)
 | 
			
		||||
    func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws {
 | 
			
		||||
        notify(accessTo: secret, from: store, by: provenance, requiredAuthentication: requiredAuthentication)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -68,10 +115,20 @@ extension Notifier: SigningWitness {
 | 
			
		||||
extension Notifier {
 | 
			
		||||
 | 
			
		||||
    enum Constants {
 | 
			
		||||
 | 
			
		||||
        // Update notifications
 | 
			
		||||
        static let updateCategoryIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.update"
 | 
			
		||||
        static let criticalUpdateCategoryIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.update.critical"
 | 
			
		||||
        static let updateActionIdentitifier  = "com.maxgoedjen.Secretive.SecretAgent.update.updateaction"
 | 
			
		||||
        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 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, 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 }
 | 
			
		||||
        switch response.actionIdentifier {
 | 
			
		||||
        case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
 | 
			
		||||
@ -95,7 +170,14 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
 | 
			
		||||
        default:
 | 
			
		||||
            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) {
 | 
			
		||||
 | 
			
		||||
@ -89,11 +89,12 @@ extension Agent {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 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)!
 | 
			
		||||
 | 
			
		||||
@ -134,7 +135,7 @@ extension Agent {
 | 
			
		||||
        signedData.append(writer.lengthAndData(of: sub))
 | 
			
		||||
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import SecretKit
 | 
			
		||||
 | 
			
		||||
public protocol SigningWitness {
 | 
			
		||||
 | 
			
		||||
    func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
 | 
			
		||||
    func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ extension Stub {
 | 
			
		||||
            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 {
 | 
			
		||||
                throw NSError(domain: "test", code: 0, userInfo: nil)
 | 
			
		||||
            }
 | 
			
		||||
@ -67,7 +67,10 @@ extension Stub {
 | 
			
		||||
            default:
 | 
			
		||||
                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 {
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        if objection {
 | 
			
		||||
            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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,9 @@ public class AnySecretStore: SecretStore {
 | 
			
		||||
    private let _id: () -> UUID
 | 
			
		||||
    private let _name: () -> String
 | 
			
		||||
    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?
 | 
			
		||||
 | 
			
		||||
    public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
 | 
			
		||||
@ -18,6 +20,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) }
 | 
			
		||||
        _persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
 | 
			
		||||
        sink = secretStore.objectWillChange.sink { _ in
 | 
			
		||||
            self.objectWillChange.send()
 | 
			
		||||
        }
 | 
			
		||||
@ -39,10 +42,14 @@ public class AnySecretStore: SecretStore {
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws {
 | 
			
		||||
        try _persistAuthentication(secret, duration)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
 | 
			
		||||
@ -69,4 +76,5 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
 | 
			
		||||
    public func update(secret: AnySecret, name: String) throws {
 | 
			
		||||
        try _update(secret, name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,9 @@ public protocol SecretStore: ObservableObject, Identifiable {
 | 
			
		||||
    var name: String { 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")
 | 
			
		||||
        @Published public private(set) var secrets: [Secret] = []
 | 
			
		||||
 | 
			
		||||
        private var persistedAuthenticationContexts: [Secret: PersistentAuthenticationContext] = [:]
 | 
			
		||||
 | 
			
		||||
        public init() {
 | 
			
		||||
            DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
 | 
			
		||||
                self.reloadSecrets(notify: false)
 | 
			
		||||
@ -93,10 +95,16 @@ extension SecureEnclave {
 | 
			
		||||
            reloadSecrets()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
 | 
			
		||||
            let context = LAContext()
 | 
			
		||||
        public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData {
 | 
			
		||||
            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.localizedCancelTitle = "Deny"
 | 
			
		||||
            let attributes = [
 | 
			
		||||
                kSecClass: kSecClassKey,
 | 
			
		||||
                kSecAttrKeyClass: kSecAttrKeyClassPrivate,
 | 
			
		||||
@ -117,10 +125,37 @@ 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)
 | 
			
		||||
            }
 | 
			
		||||
            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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension SecureEnclave {
 | 
			
		||||
@ -202,6 +238,30 @@ extension SecureEnclave {
 | 
			
		||||
    enum Constants {
 | 
			
		||||
        static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData
 | 
			
		||||
        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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,6 @@ extension SmartCard {
 | 
			
		||||
 | 
			
		||||
    public class Store: SecretStore {
 | 
			
		||||
 | 
			
		||||
        // TODO: Read actual smart card name, eg "YubiKey 5c"
 | 
			
		||||
        @Published public var isAvailable: Bool = false
 | 
			
		||||
        public let id = UUID()
 | 
			
		||||
        public private(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
 | 
			
		||||
@ -44,7 +43,7 @@ extension SmartCard {
 | 
			
		||||
            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() }
 | 
			
		||||
            let context = LAContext()
 | 
			
		||||
            context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
 | 
			
		||||
@ -79,7 +78,10 @@ extension SmartCard {
 | 
			
		||||
            guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
 | 
			
		||||
                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 {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
@ -103,12 +105,22 @@ extension SmartCard.Store {
 | 
			
		||||
 | 
			
		||||
    private func loadSecrets() {
 | 
			
		||||
        guard let tokenID = tokenID else { return }
 | 
			
		||||
        // Hack to read name if there's only one smart card
 | 
			
		||||
        let slotNames = TKSmartCardSlotManager().slotNames
 | 
			
		||||
        if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 {
 | 
			
		||||
            name = slotNames.first!
 | 
			
		||||
 | 
			
		||||
        let fallbackName = NSLocalizedString("Smart Card", comment: "Smart Card")
 | 
			
		||||
        if #available(macOS 12.0, *) {
 | 
			
		||||
            if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName {
 | 
			
		||||
                name = driverName
 | 
			
		||||
            } else {
 | 
			
		||||
                name = fallbackName
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            name = NSLocalizedString("Smart Card", comment: "Smart Card")
 | 
			
		||||
            // Hack to read name if there's only one smart card
 | 
			
		||||
            let slotNames = TKSmartCardSlotManager().slotNames
 | 
			
		||||
            if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 {
 | 
			
		||||
                name = slotNames.first!
 | 
			
		||||
            } else {
 | 
			
		||||
                name = fallbackName
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let attributes = [
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
		50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
 | 
			
		||||
		5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.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 */; };
 | 
			
		||||
		50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.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>"; };
 | 
			
		||||
		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>"; };
 | 
			
		||||
		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>"; };
 | 
			
		||||
		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>"; };
 | 
			
		||||
@ -394,6 +396,7 @@
 | 
			
		||||
				507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */,
 | 
			
		||||
				50617DCA23FCECA10099B055 /* Secret.swift */,
 | 
			
		||||
				50617DC623FCE4EA0099B055 /* SecretStore.swift */,
 | 
			
		||||
				5035FF6D2737A2F4006FE1F6 /* SignedData.swift */,
 | 
			
		||||
			);
 | 
			
		||||
			path = Types;
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
@ -1059,6 +1062,7 @@
 | 
			
		||||
				5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */,
 | 
			
		||||
				5099A02B23FE352C0062B6F2 /* SmartCardSecret.swift in Sources */,
 | 
			
		||||
				50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */,
 | 
			
		||||
				5035FF6E2737A2F4006FE1F6 /* SignedData.swift in Sources */,
 | 
			
		||||
			);
 | 
			
		||||
			runOnlyForDeploymentPostprocessing = 0;
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
@ -35,8 +35,11 @@ extension Preview {
 | 
			
		||||
            self.secrets.append(contentsOf: new)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
 | 
			
		||||
            return data
 | 
			
		||||
        func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> SignedData {
 | 
			
		||||
            return SignedData(data: data, requiredAuthentication: false)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user