mirror of
				https://github.com/maxgoedjen/secretive.git
				synced 2025-10-31 07:20:57 +00:00 
			
		
		
		
	Fix for SecretAgent acting as if it has no keys after updating macOS (#427)
* Allow reload pre-op * Fix tests * Make sure standin keys get rewritten on force update * Stub store
This commit is contained in:
		
							parent
							
								
									382913cb99
								
							
						
					
					
						commit
						3b254d33a5
					
				| @ -31,13 +31,14 @@ public class Agent { | ||||
|     private let writer = OpenSSHKeyWriter() | ||||
|     private let requestTracer = SigningRequestTracer() | ||||
|     private let certsPath = (NSHomeDirectory() as NSString).appendingPathComponent("PublicKeys") as String | ||||
|     private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent.agent", category: "") | ||||
| 
 | ||||
|     /// Initializes an agent with a store list and a witness. | ||||
|     /// - Parameters: | ||||
|     ///   - storeList: The `SecretStoreList` to make available. | ||||
|     ///   - witness: A witness to notify of requests. | ||||
|     public init(storeList: SecretStoreList, witness: SigningWitness? = nil) { | ||||
|         Logger().debug("Agent is running") | ||||
|         logger.debug("Agent is running") | ||||
|         self.storeList = storeList | ||||
|         self.witness = witness | ||||
|     } | ||||
| @ -53,16 +54,16 @@ extension Agent { | ||||
|     /// - Return value:  | ||||
|     ///   - Boolean if data could be read | ||||
|     @discardableResult public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool { | ||||
|         Logger().debug("Agent handling new data") | ||||
|         logger.debug("Agent handling new data") | ||||
|         let data = Data(reader.availableData) | ||||
|         guard data.count > 4 else { return false} | ||||
|         let requestTypeInt = data[4] | ||||
|         guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else { | ||||
|             writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data)) | ||||
|             Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") | ||||
|             logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") | ||||
|             return true | ||||
|         } | ||||
|         Logger().debug("Agent handling request of type \(requestType.debugDescription)") | ||||
|         logger.debug("Agent handling request of type \(requestType.debugDescription)") | ||||
|         let subData = Data(data[5...]) | ||||
|         let response = handle(requestType: requestType, data: subData, reader: reader) | ||||
|         writer.write(response) | ||||
| @ -70,23 +71,25 @@ extension Agent { | ||||
|     } | ||||
| 
 | ||||
|     func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data { | ||||
|         // Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting | ||||
|         reloadSecretsIfNeccessary() | ||||
|         var response = Data() | ||||
|         do { | ||||
|             switch requestType { | ||||
|             case .requestIdentities: | ||||
|                 response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data) | ||||
|                 response.append(identities()) | ||||
|                 Logger().debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)") | ||||
|                 logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)") | ||||
|             case .signRequest: | ||||
|                 let provenance = requestTracer.provenance(from: reader) | ||||
|                 response.append(SSHAgent.ResponseType.agentSignResponse.data) | ||||
|                 response.append(try sign(data: data, provenance: provenance)) | ||||
|                 Logger().debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)") | ||||
|                 logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)") | ||||
|             } | ||||
|         } catch { | ||||
|             response.removeAll() | ||||
|             response.append(SSHAgent.ResponseType.agentFailure.data) | ||||
|             Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") | ||||
|             logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") | ||||
|         } | ||||
|         let full = OpenSSHKeyWriter().lengthAndData(of: response) | ||||
|         return full | ||||
| @ -120,7 +123,7 @@ extension Agent { | ||||
|             keyData.append(writer.lengthAndData(of: curveData)) | ||||
|              | ||||
|         } | ||||
|         Logger().debug("Agent enumerated \(secrets.count) identities") | ||||
|         logger.log("Agent enumerated \(secrets.count) identities") | ||||
|         return countData + keyData | ||||
|     } | ||||
| 
 | ||||
| @ -139,7 +142,7 @@ extension Agent { | ||||
|         } | ||||
|          | ||||
|         guard let (store, secret) = secret(matching: hash) else { | ||||
|             Logger().debug("Agent did not have a key matching \(hash as NSData)") | ||||
|             logger.debug("Agent did not have a key matching \(hash as NSData)") | ||||
|             throw AgentError.noMatchingKey | ||||
|         } | ||||
| 
 | ||||
| @ -193,7 +196,7 @@ extension Agent { | ||||
|             try witness.witness(accessTo: secret, from: store, by: provenance) | ||||
|         } | ||||
| 
 | ||||
|         Logger().debug("Agent signed request") | ||||
|         logger.debug("Agent signed request") | ||||
| 
 | ||||
|         return signedData | ||||
|     } | ||||
| @ -235,7 +238,7 @@ extension Agent { | ||||
|         let certificatePath = certsPath.appending("/").appending("\(minimalHex)-cert.pub") | ||||
|          | ||||
|         if FileManager.default.fileExists(atPath: certificatePath) { | ||||
|             Logger().debug("Found certificate for \(secret.name)") | ||||
|             logger.debug("Found certificate for \(secret.name)") | ||||
|             do { | ||||
|                 let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8) | ||||
|                 let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ") | ||||
| @ -246,19 +249,19 @@ extension Agent { | ||||
|                             if let certName = certElements[2].data(using: .utf8) { | ||||
|                                 return (certDecoded, certName) | ||||
|                             } else if let certName = secret.name.data(using: .utf8) { | ||||
|                                 Logger().info("Certificate for \(secret.name) does not have a name tag, using secret name instead") | ||||
|                                 logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead") | ||||
|                                 return (certDecoded, certName) | ||||
|                             } else { | ||||
|                                 throw OpenSSHCertificateError.parsingFailed | ||||
|                             } | ||||
|                         } | ||||
|                     } else { | ||||
|                         Logger().warning("Certificate found for \(secret.name) but failed to decode base64 key") | ||||
|                         logger.warning("Certificate found for \(secret.name) but failed to decode base64 key") | ||||
|                         throw OpenSSHCertificateError.parsingFailed | ||||
|                     } | ||||
|                 } | ||||
|             } catch { | ||||
|                 Logger().warning("Certificate found for \(secret.name) but failed to load") | ||||
|                 logger.warning("Certificate found for \(secret.name) but failed to load") | ||||
|                 throw OpenSSHCertificateError.parsingFailed | ||||
|             } | ||||
|         } | ||||
| @ -270,6 +273,16 @@ extension Agent { | ||||
| 
 | ||||
| extension Agent { | ||||
| 
 | ||||
|     /// Gives any store with no loaded secrets a chance to reload. | ||||
|     func reloadSecretsIfNeccessary() { | ||||
|         for store in storeList.stores { | ||||
|             if store.secrets.isEmpty { | ||||
|                 logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.") | ||||
|                 store.reloadSecrets() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Finds a ``Secret`` matching a specified hash whos signature was requested. | ||||
|     /// - Parameter hash: The hash to match against. | ||||
|     /// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found. | ||||
|  | ||||
| @ -12,6 +12,7 @@ public class AnySecretStore: SecretStore { | ||||
|     private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data | ||||
|     private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext? | ||||
|     private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void | ||||
|     private let _reloadSecrets: () -> Void | ||||
| 
 | ||||
|     private var sink: AnyCancellable? | ||||
| 
 | ||||
| @ -24,6 +25,7 @@ public class AnySecretStore: SecretStore { | ||||
|         _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) } | ||||
|         _reloadSecrets = { secretStore.reloadSecrets() } | ||||
|         sink = secretStore.objectWillChange.sink { _ in | ||||
|             self.objectWillChange.send() | ||||
|         } | ||||
| @ -57,6 +59,10 @@ public class AnySecretStore: SecretStore { | ||||
|         try _persistAuthentication(secret, duration) | ||||
|     } | ||||
| 
 | ||||
|     public func reloadSecrets() { | ||||
|         _reloadSecrets() | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable { | ||||
|  | ||||
| @ -36,6 +36,9 @@ public protocol SecretStore: ObservableObject, Identifiable { | ||||
|     ///  - Note: This is used for temporarily unlocking access to a secret which would otherwise require authentication every single use. This is useful for situations where the user anticipates several rapid accesses to a authorization-guarded secret. | ||||
|     func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) throws | ||||
| 
 | ||||
|     /// Requests that the store reload secrets from any backing store, if neccessary. | ||||
|     func reloadSecrets() | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /// A SecretStore that the Secretive admin app can modify. | ||||
|  | ||||
| @ -24,7 +24,7 @@ extension SecureEnclave { | ||||
|         /// Initializes a Store. | ||||
|         public init() { | ||||
|             DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in | ||||
|                 self.reloadSecrets(notifyAgent: false) | ||||
|                 self.reloadSecretsInternal(notifyAgent: false) | ||||
|             } | ||||
|             loadSecrets() | ||||
|         } | ||||
| @ -68,7 +68,7 @@ extension SecureEnclave { | ||||
|                 throw KeychainError(statusCode: nil) | ||||
|             } | ||||
|             try savePublicKey(publicKey, name: name) | ||||
|             reloadSecrets() | ||||
|             reloadSecretsInternal() | ||||
|         } | ||||
| 
 | ||||
|         public func delete(secret: Secret) throws { | ||||
| @ -80,7 +80,7 @@ extension SecureEnclave { | ||||
|             if status != errSecSuccess { | ||||
|                 throw KeychainError(statusCode: status) | ||||
|             } | ||||
|             reloadSecrets() | ||||
|             reloadSecretsInternal() | ||||
|         } | ||||
| 
 | ||||
|         public func update(secret: Secret, name: String) throws { | ||||
| @ -97,7 +97,7 @@ extension SecureEnclave { | ||||
|             if status != errSecSuccess { | ||||
|                 throw KeychainError(statusCode: status) | ||||
|             } | ||||
|             reloadSecrets() | ||||
|             reloadSecretsInternal() | ||||
|         } | ||||
|          | ||||
|         public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data { | ||||
| @ -163,6 +163,10 @@ extension SecureEnclave { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public func reloadSecrets() { | ||||
|             reloadSecretsInternal(notifyAgent: false) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -171,12 +175,15 @@ extension SecureEnclave.Store { | ||||
| 
 | ||||
|     /// Reloads all secrets from the store. | ||||
|     /// - Parameter notifyAgent: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well. | ||||
|     private func reloadSecrets(notifyAgent: Bool = true) { | ||||
|     private func reloadSecretsInternal(notifyAgent: Bool = true) { | ||||
|         let before = secrets | ||||
|         secrets.removeAll() | ||||
|         loadSecrets() | ||||
|         NotificationCenter.default.post(name: .secretStoreReloaded, object: self) | ||||
|         if notifyAgent { | ||||
|             DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true) | ||||
|         if secrets != before { | ||||
|             NotificationCenter.default.post(name: .secretStoreReloaded, object: self) | ||||
|             if notifyAgent { | ||||
|                 DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -89,6 +89,19 @@ extension SmartCard { | ||||
|         public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws { | ||||
|         } | ||||
| 
 | ||||
|         /// Reloads all secrets from the store. | ||||
|         public func reloadSecrets() { | ||||
|             DispatchQueue.main.async { | ||||
|                 self.isAvailable = self.tokenID != nil | ||||
|                 let before = self.secrets | ||||
|                 self.secrets.removeAll() | ||||
|                 self.loadSecrets() | ||||
|                 if self.secrets != before { | ||||
|                     NotificationCenter.default.post(name: .secretStoreReloaded, object: self) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -102,15 +115,6 @@ extension SmartCard.Store { | ||||
|         reloadSecrets() | ||||
|     } | ||||
| 
 | ||||
|     /// Reloads all secrets from the store. | ||||
|     private func reloadSecrets() { | ||||
|         DispatchQueue.main.async { | ||||
|             self.isAvailable = self.tokenID != nil | ||||
|             self.secrets.removeAll() | ||||
|             self.loadSecrets() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Loads all secrets from the store. | ||||
|     private func loadSecrets() { | ||||
|         guard let tokenID = tokenID else { return } | ||||
|  | ||||
| @ -78,6 +78,9 @@ extension Stub { | ||||
|         public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws { | ||||
|         } | ||||
| 
 | ||||
|         public func reloadSecrets() { | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -47,6 +47,9 @@ extension Preview { | ||||
|         func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws { | ||||
|         } | ||||
| 
 | ||||
|         func reloadSecrets() { | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     class StoreModifiable: Store, SecretStoreModifiable { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user