Compare commits

..

6 Commits

Author SHA1 Message Date
64b2cc6f29 Fix 2022-06-18 20:19:11 -07:00
84dd9403c3 Update FAQ.md (#361) 2022-02-26 22:56:09 +00:00
0af7b803bc Add section on SSH agent forwarding to FAQ (#360) 2022-02-26 22:48:24 +00:00
a1009d0dac Statically load whether or not a key requires authentication before use (#357)
* Add protocol requirements

* Load auth settings.

* Updates.

* Update preview store

* Add lock icon
2022-02-25 06:59:35 +00:00
ae7394f771 Update Agent.swift (#359) 2022-02-25 06:57:29 +00:00
6ea0a3ebd2 Update screenshots (#352)
* Update

* Update README.md
2022-02-17 07:12:14 +00:00
24 changed files with 142 additions and 73 deletions

BIN
.github/readme/app.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 259 KiB

6
FAQ.md
View File

@ -26,7 +26,11 @@ Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [
### How do I tell SSH to use a specific key? ### How do I tell SSH to use a specific key?
You can create a `mykey.pub` (where `mykey` is the name of your key) in your `~/.ssh/` directory with the contents of your public key, and specify that you want to use that key in your `~/.ssh/config`. [This ServerFault answer](https://serverfault.com/a/295771) has more details on setting that up Beginning with Secretive 2.2, every secret has an automatically generated public key file representation on disk, and the path to it is listed under "Public Key Path" in Secretive. You can specify that you want to use that key in your `~/.ssh/config`. [This ServerFault answer](https://serverfault.com/a/295771) has more details on setting that up.
### Can I use Secretive for SSH Agent Forwarding?
Yes, you can! Once you've set up Secretive, just add `ForwardAgent yes` to the hosts you want to forward to in your SSH config file. Afterwards, any use of one of your SSH keys on the remote host must be authenticated through Secretive.
### Why should I trust you? ### Why should I trust you?

View File

@ -16,13 +16,13 @@ The most common setup for SSH keys is just keeping them on disk, guarded by prop
If your Mac has a Secure Enclave, it also has support for strong access controls like Touch ID, or authentication with Apple Watch. You can configure your key so that they require Touch ID (or Watch) authentication before they're accessed. If your Mac has a Secure Enclave, it also has support for strong access controls like Touch ID, or authentication with Apple Watch. You can configure your key so that they require Touch ID (or Watch) authentication before they're accessed.
<img src="/.github/readme/touchid.png" alt="Screenshot of Secretive authenticating with Touch ID"> <img src="/.github/readme/touchid.png" alt="Screenshot of Secretive authenticating with Touch ID" width="400">
### Notifications ### Notifications
Secretive also notifies you whenever your keys are accessed, so you're never caught off guard. Secretive also notifies you whenever your keys are accessed, so you're never caught off guard.
<img src="/.github/readme/notification.png" alt="Screenshot of Secretive notifying the user"> <img src="/.github/readme/notification.png" alt="Screenshot of Secretive notifying the user" width="600">
### Support for Smart Cards Too! ### Support for Smart Cards Too!

View File

@ -32,7 +32,7 @@ extension Agent {
/// - writer: A ``FileHandleWriter`` to write the response to. /// - writer: A ``FileHandleWriter`` to write the response to.
/// - Return value: /// - Return value:
/// - Boolean if data could be read /// - Boolean if data could be read
public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool { @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) let data = Data(reader.availableData)
guard data.count > 4 else { return false} guard data.count > 4 else { return false}
@ -113,7 +113,7 @@ extension Agent {
let dataToSign = reader.readNextChunk() let dataToSign = reader.readNextChunk()
let signed = 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 derSignature = signed
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)! 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)) signedData.append(writer.lengthAndData(of: sub))
if let witness = witness { 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") Logger().debug("Agent signed request")

View File

@ -17,7 +17,6 @@ public protocol SigningWitness {
/// - secret: The `Secret` that will was used to sign the request. /// - secret: The `Secret` that will was used to sign the request.
/// - store: The `Store` that signed the request.. /// - store: The `Store` that signed the request..
/// - provenance: A `SigningRequestProvenance` object describing the origin of 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) throws
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws
} }

View File

@ -45,20 +45,14 @@ public class SocketController {
var addr = sockaddr_un() var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX) addr.sun_family = sa_family_t(AF_UNIX)
var len: Int = 0 let len = MemoryLayout.size(ofValue: addr.sun_path) - 1
withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
path.withCString { cstring in // The buffer is pre-zeroed, so manual termination is unnecessary.
len = strlen(cstring) precondition(memccpy(pointer, path, 0, len) != nil)
strncpy(pointer, cstring, len)
}
}
addr.sun_len = UInt8(len+2)
var data: Data!
withUnsafePointer(to: &addr) { pointer in
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
} }
addr.sun_len = UInt8(len)
let data = withUnsafeBytes(of: &addr, Data.init(_:))
return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)! return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!
} }

View File

@ -27,5 +27,8 @@ SecretKit is a collection of protocols describing secrets and stores.
### Signing Process ### Signing Process
- ``SignedData``
- ``SigningRequestProvenance`` - ``SigningRequestProvenance``
### Authentication Persistence
- ``PersistedAuthenticationContext``

View File

@ -9,6 +9,7 @@ public struct AnySecret: Secret {
private let _name: () -> String private let _name: () -> String
private let _algorithm: () -> Algorithm private let _algorithm: () -> Algorithm
private let _keySize: () -> Int private let _keySize: () -> Int
private let _requiresAuthentication: () -> Bool
private let _publicKey: () -> Data private let _publicKey: () -> Data
public init<T>(_ secret: T) where T: Secret { public init<T>(_ secret: T) where T: Secret {
@ -19,6 +20,7 @@ public struct AnySecret: Secret {
_name = secret._name _name = secret._name
_algorithm = secret._algorithm _algorithm = secret._algorithm
_keySize = secret._keySize _keySize = secret._keySize
_requiresAuthentication = secret._requiresAuthentication
_publicKey = secret._publicKey _publicKey = secret._publicKey
} else { } else {
base = secret as Any base = secret as Any
@ -27,6 +29,7 @@ public struct AnySecret: Secret {
_name = { secret.name } _name = { secret.name }
_algorithm = { secret.algorithm } _algorithm = { secret.algorithm }
_keySize = { secret.keySize } _keySize = { secret.keySize }
_requiresAuthentication = { secret.requiresAuthentication }
_publicKey = { secret.publicKey } _publicKey = { secret.publicKey }
} }
} }
@ -47,6 +50,10 @@ public struct AnySecret: Secret {
_keySize() _keySize()
} }
public var requiresAuthentication: Bool {
_requiresAuthentication()
}
public var publicKey: Data { public var publicKey: Data {
_publicKey() _publicKey()
} }

View File

@ -9,7 +9,8 @@ public class AnySecretStore: SecretStore {
private let _id: () -> UUID private let _id: () -> UUID
private let _name: () -> String private let _name: () -> String
private let _secrets: () -> [AnySecret] 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 let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
private var sink: AnyCancellable? private var sink: AnyCancellable?
@ -21,6 +22,7 @@ public class AnySecretStore: SecretStore {
_id = { secretStore.id } _id = { secretStore.id }
_secrets = { secretStore.secrets.map { AnySecret($0) } } _secrets = { secretStore.secrets.map { AnySecret($0) } }
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) } _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) } _persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
sink = secretStore.objectWillChange.sink { _ in sink = secretStore.objectWillChange.sink { _ in
self.objectWillChange.send() self.objectWillChange.send()
@ -43,10 +45,14 @@ public class AnySecretStore: SecretStore {
return _secrets() 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) try _sign(data, secret, provenance)
} }
public func existingPersistedAuthenticationContext(secret: AnySecret) -> PersistedAuthenticationContext? {
_existingPersistedAuthenticationContext(secret)
}
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws { public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws {
try _persistAuthentication(secret, duration) try _persistAuthentication(secret, duration)
} }

View File

@ -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 }
}

View File

@ -9,6 +9,8 @@ public protocol Secret: Identifiable, Hashable {
var algorithm: Algorithm { get } var algorithm: Algorithm { get }
/// The key size for the secret. /// The key size for the secret.
var keySize: Int { get } var keySize: Int { get }
/// Whether the secret requires authentication before use.
var requiresAuthentication: Bool { get }
/// The public key data for the secret. /// The public key data for the secret.
var publicKey: Data { get } var publicKey: Data { get }

View File

@ -20,8 +20,14 @@ public protocol SecretStore: ObservableObject, Identifiable {
/// - data: The data to sign. /// - data: The data to sign.
/// - secret: The ``Secret`` to sign with. /// - secret: The ``Secret`` to sign with.
/// - provenance: A ``SigningRequestProvenance`` describing where the request came from. /// - provenance: A ``SigningRequestProvenance`` describing where the request came from.
/// - Returns: A ``SignedData`` object, containing the signature and metadata about the signature process. /// - Returns: The signed data.
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData 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. /// Persists user authorization for access to a secret.
/// - Parameters: /// - Parameters:

View File

@ -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
}
}

View File

@ -11,6 +11,7 @@ extension SecureEnclave {
public let name: String public let name: String
public let algorithm = Algorithm.ellipticCurve public let algorithm = Algorithm.ellipticCurve
public let keySize = 256 public let keySize = 256
public let requiresAuthentication: Bool
public let publicKey: Data public let publicKey: Data
} }

View File

@ -100,7 +100,7 @@ extension SecureEnclave {
reloadSecrets() 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 let context: LAContext
if let existing = persistedAuthenticationContexts[secret], existing.valid { if let existing = persistedAuthenticationContexts[secret], existing.valid {
context = existing.context context = existing.context
@ -131,16 +131,15 @@ extension SecureEnclave {
let key = untypedSafe as! SecKey let key = untypedSafe as! SecKey
var signError: SecurityError? var signError: SecurityError?
let signingStartTime = Date()
guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else { guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else {
throw SigningError(error: signError) throw SigningError(error: signError)
} }
let signatureDuration = Date().timeIntervalSince(signingStartTime) return signature as Data
// 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 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 { public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) throws {
@ -183,7 +182,7 @@ extension SecureEnclave.Store {
/// Loads all secrets from the store. /// Loads all secrets from the store.
private func loadSecrets() { private func loadSecrets() {
let attributes = [ let publicAttributes = [
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyType: SecureEnclave.Constants.keyType, kSecAttrKeyType: SecureEnclave.Constants.keyType,
kSecAttrApplicationTag: SecureEnclave.Constants.keyTag, kSecAttrApplicationTag: SecureEnclave.Constants.keyTag,
@ -192,16 +191,46 @@ extension SecureEnclave.Store {
kSecMatchLimit: kSecMatchLimitAll, kSecMatchLimit: kSecMatchLimitAll,
kSecReturnAttributes: true kSecReturnAttributes: true
] as CFDictionary ] as CFDictionary
var untyped: CFTypeRef? var publicUntyped: CFTypeRef?
SecItemCopyMatching(attributes, &untyped) SecItemCopyMatching(publicAttributes, &publicUntyped)
guard let typed = untyped as? [[CFString: Any]] else { return } guard let publicTyped = publicUntyped as? [[CFString: Any]] else { return }
let wrapped: [SecureEnclave.Secret] = typed.map { 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 name = $0[kSecAttrLabel] as? String ?? "Unnamed"
let id = $0[kSecAttrApplicationLabel] as! Data let id = $0[kSecAttrApplicationLabel] as! Data
let publicKeyRef = $0[kSecValueRef] as! SecKey let publicKeyRef = $0[kSecValueRef] as! SecKey
let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any] let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any]
let publicKey = publicKeyAttributes[kSecValueData] as! Data 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) secrets.append(contentsOf: wrapped)
} }
@ -264,7 +293,7 @@ extension SecureEnclave {
extension SecureEnclave { extension SecureEnclave {
/// A context describing a persisted authentication. /// A context describing a persisted authentication.
private struct PersistentAuthenticationContext { private struct PersistentAuthenticationContext: PersistedAuthenticationContext {
/// The Secret to persist authentication for. /// The Secret to persist authentication for.
let secret: Secret let secret: Secret
@ -272,7 +301,7 @@ extension SecureEnclave {
let context: LAContext let context: LAContext
/// An expiration date for the context. /// An expiration date for the context.
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back. /// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
let expiration: UInt64 let monotonicExpiration: UInt64
/// Initializes a context. /// Initializes a context.
/// - Parameters: /// - Parameters:
@ -283,12 +312,18 @@ extension SecureEnclave {
self.secret = secret self.secret = secret
self.context = context self.context = context
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value 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. /// A boolean describing whether or not the context is still valid.
var valid: Bool { 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)
} }
} }

View File

@ -11,6 +11,7 @@ extension SmartCard {
public let name: String public let name: String
public let algorithm: Algorithm public let algorithm: Algorithm
public let keySize: Int public let keySize: Int
public let requiresAuthentication: Bool = false
public let publicKey: Data public let publicKey: Data
} }

View File

@ -44,7 +44,7 @@ extension SmartCard {
fatalError("Keys must be deleted on the smart card.") 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() } guard let tokenID = tokenID else { fatalError() }
let context = LAContext() let context = LAContext()
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\"" 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 { guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
throw SigningError(error: signError) 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 { public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws {

View File

@ -49,7 +49,7 @@ extension Stub {
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))") 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 { guard !shouldThrow else {
throw NSError(domain: "test", code: 0, userInfo: nil) throw NSError(domain: "test", code: 0, userInfo: nil)
} }
@ -68,7 +68,11 @@ extension Stub {
default: default:
fatalError() 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 { public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws {
@ -88,6 +92,7 @@ extension Stub {
let keySize: Int let keySize: Int
let publicKey: Data let publicKey: Data
let requiresAuthentication = false
let privateKey: Data let privateKey: Data
init(keySize: Int, publicKey: Data, privateKey: Data) { init(keySize: Int, publicKey: Data, privateKey: Data) {

View File

@ -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) witness(secret, provenance)
} }

View File

@ -57,7 +57,7 @@ class Notifier {
notificationCenter.requestAuthorization(options: .alert) { _, _ in } 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.pendingPersistableSecrets[secret.id.description] = secret
notificationDelegate.pendingPersistableStores[store.id.description] = store notificationDelegate.pendingPersistableStores[store.id.description] = store
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
@ -69,7 +69,7 @@ class Notifier {
if #available(macOS 12.0, *) { if #available(macOS 12.0, *) {
notificationContent.interruptionLevel = .timeSensitive notificationContent.interruptionLevel = .timeSensitive
} }
if requiredAuthentication { if secret.requiresAuthentication && store.existingPersistedAuthenticationContext(secret: secret) == nil {
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
} }
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) { 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 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 { func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
notify(accessTo: secret, from: store, by: provenance, requiredAuthentication: requiredAuthentication) notify(accessTo: secret, from: store, by: provenance)
} }
} }

View File

@ -11,6 +11,7 @@ extension Preview {
let name: String let name: String
let algorithm = Algorithm.ellipticCurve let algorithm = Algorithm.ellipticCurve
let keySize = 256 let keySize = 256
let requiresAuthentication: Bool = false
let publicKey = UUID().uuidString.data(using: .utf8)! let publicKey = UUID().uuidString.data(using: .utf8)!
} }
@ -35,8 +36,12 @@ extension Preview {
self.secrets.append(contentsOf: new) self.secrets.append(contentsOf: new)
} }
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> SignedData { func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
return SignedData(data: data, requiredAuthentication: false) return data
}
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
nil
} }
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws { func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {

View File

@ -20,7 +20,15 @@ struct SecretListItemView: View {
) )
return NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) { 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 { }.contextMenu {
if store is AnySecretStoreModifiable { if store is AnySecretStoreModifiable {
Button(action: { isRenaming = true }) { Button(action: { isRenaming = true }) {