diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index 89621ef..acf3cde 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/AnySecret.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift index b5a748d..f6e8bef 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift @@ -9,6 +9,7 @@ public struct AnySecret: Secret { private let _name: () -> String private let _algorithm: () -> Algorithm private let _keySize: () -> Int + private let _requiresAuthentication: () -> Bool private let _publicKey: () -> Data public init(_ secret: T) where T: Secret { @@ -19,6 +20,7 @@ public struct AnySecret: Secret { _name = secret._name _algorithm = secret._algorithm _keySize = secret._keySize + _requiresAuthentication = secret._requiresAuthentication _publicKey = secret._publicKey } else { base = secret as Any @@ -27,6 +29,7 @@ public struct AnySecret: Secret { _name = { secret.name } _algorithm = { secret.algorithm } _keySize = { secret.keySize } + _requiresAuthentication = { secret.requiresAuthentication } _publicKey = { secret.publicKey } } } @@ -47,6 +50,10 @@ public struct AnySecret: Secret { _keySize() } + public var requiresAuthentication: Bool { + _requiresAuthentication() + } + public var publicKey: Data { _publicKey() } 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/Secret.swift b/Sources/Packages/Sources/SecretKit/Types/Secret.swift index 154a2d3..6fc57a1 100644 --- a/Sources/Packages/Sources/SecretKit/Types/Secret.swift +++ b/Sources/Packages/Sources/SecretKit/Types/Secret.swift @@ -9,6 +9,8 @@ public protocol Secret: Identifiable, Hashable { var algorithm: Algorithm { get } /// The key size for the secret. var keySize: Int { get } + /// Whether the secret requires authentication before use. + var requiresAuthentication: Bool { get } /// The public key data for the secret. var publicKey: Data { 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/SecureEnclaveSecret.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift index c8a87ba..530d01e 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift @@ -11,6 +11,7 @@ extension SecureEnclave { public let name: String public let algorithm = Algorithm.ellipticCurve public let keySize = 256 + public let requiresAuthentication: Bool public let publicKey: Data } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index a902901..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 { @@ -183,7 +182,7 @@ extension SecureEnclave.Store { /// Loads all secrets from the store. private func loadSecrets() { - let attributes = [ + let publicAttributes = [ kSecClass: kSecClassKey, kSecAttrKeyType: SecureEnclave.Constants.keyType, kSecAttrApplicationTag: SecureEnclave.Constants.keyTag, @@ -192,16 +191,46 @@ extension SecureEnclave.Store { kSecMatchLimit: kSecMatchLimitAll, kSecReturnAttributes: true ] as CFDictionary - var untyped: CFTypeRef? - SecItemCopyMatching(attributes, &untyped) - guard let typed = untyped as? [[CFString: Any]] else { return } - let wrapped: [SecureEnclave.Secret] = typed.map { + var publicUntyped: CFTypeRef? + SecItemCopyMatching(publicAttributes, &publicUntyped) + guard let publicTyped = publicUntyped as? [[CFString: Any]] else { return } + let privateAttributes = [ + kSecClass: kSecClassKey, + kSecAttrKeyType: SecureEnclave.Constants.keyType, + kSecAttrApplicationTag: SecureEnclave.Constants.keyTag, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + kSecReturnRef: true, + kSecMatchLimit: kSecMatchLimitAll, + kSecReturnAttributes: true + ] as CFDictionary + var privateUntyped: CFTypeRef? + SecItemCopyMatching(privateAttributes, &privateUntyped) + guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return } + let privateMapped = privateTyped.reduce(into: [:] as [Data: [CFString: Any]]) { partialResult, next in + let id = next[kSecAttrApplicationLabel] as! Data + partialResult[id] = next + } + let authNotRequiredAccessControl: SecAccessControl = + SecAccessControlCreateWithFlags(kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + [.privateKeyUsage], + nil)! + + let wrapped: [SecureEnclave.Secret] = publicTyped.map { let name = $0[kSecAttrLabel] as? String ?? "Unnamed" let id = $0[kSecAttrApplicationLabel] as! Data let publicKeyRef = $0[kSecValueRef] as! SecKey let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any] let publicKey = publicKeyAttributes[kSecValueData] as! Data - return SecureEnclave.Secret(id: id, name: name, publicKey: publicKey) + let privateKey = privateMapped[id] + let requiresAuth: Bool + if let authRequirements = privateKey?[kSecAttrAccessControl] { + // Unfortunately we can't inspect the access control object directly, but it does behave predicatable with equality. + requiresAuth = authRequirements as! SecAccessControl != authNotRequiredAccessControl + } else { + requiresAuth = false + } + return SecureEnclave.Secret(id: id, name: name, requiresAuthentication: requiresAuth, publicKey: publicKey) } secrets.append(contentsOf: wrapped) } @@ -264,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 @@ -272,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: @@ -283,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/SmartCardSecret.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardSecret.swift index c5e9109..655214f 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardSecret.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardSecret.swift @@ -11,6 +11,7 @@ extension SmartCard { public let name: String public let algorithm: Algorithm public let keySize: Int + public let requiresAuthentication: Bool = false public let publicKey: Data } 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/Packages/Tests/SecretAgentKitTests/StubStore.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift index e92a68e..a4fd344 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift @@ -49,7 +49,7 @@ extension Stub { print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))") } - public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> SignedData { + public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { guard !shouldThrow else { throw NSError(domain: "test", code: 0, userInfo: nil) } @@ -68,7 +68,11 @@ extension Stub { default: fatalError() } - return SignedData(data: SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data, requiredAuthentication: false) + return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data + } + + public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? { + nil } public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws { @@ -88,6 +92,7 @@ extension Stub { let keySize: Int let publicKey: Data + let requiresAuthentication = false let privateKey: Data init(keySize: Int, publicKey: Data, privateKey: Data) { diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift index 965ce5f..87e0fd9 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubWitness.swift @@ -17,7 +17,7 @@ func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: A } } -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 { witness(secret, provenance) } 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 4d4fbdb..a557f36 100644 --- a/Sources/Secretive/Preview Content/PreviewStore.swift +++ b/Sources/Secretive/Preview Content/PreviewStore.swift @@ -11,6 +11,7 @@ extension Preview { let name: String let algorithm = Algorithm.ellipticCurve let keySize = 256 + let requiresAuthentication: Bool = false let publicKey = UUID().uuidString.data(using: .utf8)! } @@ -35,8 +36,12 @@ 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 existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? { + nil } func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws { diff --git a/Sources/Secretive/Views/SecretListItemView.swift b/Sources/Secretive/Views/SecretListItemView.swift index 22c1f99..e70c47d 100644 --- a/Sources/Secretive/Views/SecretListItemView.swift +++ b/Sources/Secretive/Views/SecretListItemView.swift @@ -20,7 +20,15 @@ struct SecretListItemView: View { ) return NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) { - Text(secret.name) + if secret.requiresAuthentication { + HStack { + Text(secret.name) + Spacer() + Image(systemName: "lock") + } + } else { + Text(secret.name) + } }.contextMenu { if store is AnySecretStoreModifiable { Button(action: { isRenaming = true }) {