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