Merge branch 'filehandle_session' of github.com:maxgoedjen/secretive into filehandle_session

This commit is contained in:
Max Goedjen 2025-08-26 23:27:57 -07:00
commit f735b9b7f6
No known key found for this signature in database
10 changed files with 73 additions and 172 deletions

View File

@ -1,125 +1,3 @@
# Setting up Third Party Apps FAQ # App Configuration
## Tower Instructions for setting up apps and shells has moved to [secretive-config-instructions](https://github.com/maxgoedjen/secretive-config-instructions)!
Tower provides [instructions](https://www.git-tower.com/help/mac/integration/environment).
## GitHub Desktop
Should just work, no configuration needed
## Fork
Add this to your `~/.ssh/config` (the path should match the socket path from the setup flow).
```
Host *
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
```
## VS Code
Add this to your `~/.ssh/config` (the path should match the socket path from the setup flow).
```
Host *
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
```
## nushell
Add this to your `~/.ssh/config` (the path should match the socket path from the setup flow).
```
Host *
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
```
## Cyberduck
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>link-ssh-auth-sock</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>/bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
```
Log out and log in again before launching Cyberduck.
## Mountain Duck
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>link-ssh-auth-sock</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>/bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
```
Log out and log in again before launching Mountain Duck.
## GitKraken
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>link-ssh-auth-sock</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>/bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
```
Log out and log in again before launching Gitkraken. Then enable "Use local SSH agent in GitKraken Preferences (Located under Preferences -> SSH)
## Retcon
Add this to your `~/.ssh/config` (the path should match the socket path from the setup flow).
```
Host *
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
```
# The app I use isn't listed here!
If you know how to get it set up, please open a PR for this page and add it! Contributions are very welcome.
If you're not able to get it working, please file a [GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) for it. No guarantees we'll be able to get it working, but chances are someone else in the community might be able to.

2
FAQ.md
View File

@ -6,7 +6,7 @@ The secure enclave doesn't allow import or export of private keys. For any new c
### Secretive doesn't work with my git client/app ### Secretive doesn't work with my git client/app
Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. The `git` and `ssh` command line tools natively respect this, but third party apps may require some configuration to work. A non-exhaustive list of setup steps is provided in the [App Config FAQ](APP_CONFIG.md). Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. The `git` and `ssh` command line tools natively respect this, but third party apps may require some configuration to work. A non-exhaustive list of setup steps is provided in the [secretive-config-instructions](https://github.com/maxgoedjen/secretive-config-instructions) repo.
### Secretive isn't working for me ### Secretive isn't working for me

View File

@ -1,4 +1,4 @@
# Secretive ![Test](https://github.com/maxgoedjen/secretive/workflows/Test/badge.svg) ![Release](https://github.com/maxgoedjen/secretive/workflows/Release/badge.svg) # Secretive [![Test](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) ![Release](https://github.com/maxgoedjen/secretive/workflows/Release/badge.svg)
Secretive is an app for storing and managing SSH keys in the Secure Enclave. It is inspired by the [sekey project](https://github.com/sekey/sekey), but rewritten in Swift with no external dependencies and with a handy native management app. Secretive is an app for storing and managing SSH keys in the Secure Enclave. It is inspired by the [sekey project](https://github.com/sekey/sekey), but rewritten in Swift with no external dependencies and with a handy native management app.

View File

@ -61,20 +61,21 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable, @unchecked Sendable { public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable, @unchecked Sendable {
private let _create: @Sendable (String, Attributes) async throws -> Void private let _create: @Sendable (String, Attributes) async throws -> SecretType
private let _delete: @Sendable (AnySecret) async throws -> Void private let _delete: @Sendable (AnySecret) async throws -> Void
private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
private let _supportedKeyTypes: @Sendable () -> [KeyType] private let _supportedKeyTypes: @Sendable () -> [KeyType]
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { try await secretStore.create(name: $0, attributes: $1) } _create = { try await secretStore.create(name: $0, attributes: $1) as! SecretType }
_delete = { try await secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) } _delete = { try await secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
_update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1, attributes: $2) } _update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1, attributes: $2) }
_supportedKeyTypes = { secretStore.supportedKeyTypes } _supportedKeyTypes = { secretStore.supportedKeyTypes }
super.init(secretStore) super.init(secretStore)
} }
public func create(name: String, attributes: Attributes) async throws { @discardableResult
public func create(name: String, attributes: Attributes) async throws -> SecretType {
try await _create(name, attributes) try await _create(name, attributes)
} }

View File

@ -53,12 +53,12 @@ public extension SecretStore {
/// - secret: The secret which will be used for signing. /// - secret: The secret which will be used for signing.
/// - Returns: The appropriate algorithm. /// - Returns: The appropriate algorithm.
func signatureAlgorithm(for secret: SecretType) -> SecKeyAlgorithm? { func signatureAlgorithm(for secret: SecretType) -> SecKeyAlgorithm? {
switch (secret.keyType.algorithm, secret.keyType.size) { switch secret.keyType {
case (.ecdsa, 256): case .ecdsa256:
.ecdsaSignatureMessageX962SHA256 .ecdsaSignatureMessageX962SHA256
case (.ecdsa, 384): case .ecdsa384:
.ecdsaSignatureMessageX962SHA384 .ecdsaSignatureMessageX962SHA384
case (.rsa, 2048): case .rsa2048:
.rsaSignatureMessagePKCS1v15SHA512 .rsaSignatureMessagePKCS1v15SHA512
default: default:
nil nil

View File

@ -75,16 +75,16 @@ extension OpenSSHPublicKeyWriter {
/// - length: The key length of the algorithm. /// - length: The key length of the algorithm.
/// - Returns: The OpenSSH identifier for the algorithm. /// - Returns: The OpenSSH identifier for the algorithm.
public func openSSHIdentifier(for keyType: KeyType) -> String { public func openSSHIdentifier(for keyType: KeyType) -> String {
switch (keyType.algorithm, keyType.size) { switch keyType {
case (.ecdsa, 256): case .ecdsa256:
"ecdsa-sha2-nistp256" "ecdsa-sha2-nistp256"
case (.ecdsa, 384): case .ecdsa384:
"ecdsa-sha2-nistp384" "ecdsa-sha2-nistp384"
case (.mldsa, 65): case .mldsa65:
"ssh-mldsa-65" "ssh-mldsa-65"
case (.mldsa, 87): case .mldsa87:
"ssh-mldsa-87" "ssh-mldsa-87"
case (.rsa, _): case .rsa2048:
"ssh-rsa" "ssh-rsa"
default: default:
"unknown" "unknown"
@ -101,8 +101,7 @@ extension OpenSSHPublicKeyWriter {
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e] // [4 byte prefix][2 byte prefix][n][2 byte prefix][e]
// Rather than parse out the whole ASN.1 blob, we'll cheat and pull values directly since // Rather than parse out the whole ASN.1 blob, we'll cheat and pull values directly since
// we only support one key type, and the keychain always gives it in a specific format. // we only support one key type, and the keychain always gives it in a specific format.
let keySize = secret.keyType.size guard secret.keyType == .rsa2048 else { fatalError() }
guard secret.keyType.algorithm == .rsa && keySize == 2048 else { fatalError() }
let length = secret.keyType.size/8 let length = secret.keyType.size/8
let data = secret.publicKey let data = secret.publicKey
let n = Data(data[8..<(9+length)]) let n = Data(data[8..<(9+length)])

View File

@ -32,7 +32,13 @@ public extension Secret {
/// The type of algorithm the Secret uses. /// The type of algorithm the Secret uses.
public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible { public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible {
public static let ecdsa256 = KeyType(algorithm: .ecdsa, size: 256)
public static let ecdsa384 = KeyType(algorithm: .ecdsa, size: 384)
public static let mldsa65 = KeyType(algorithm: .mldsa, size: 65)
public static let mldsa87 = KeyType(algorithm: .mldsa, size: 87)
public static let rsa2048 = KeyType(algorithm: .rsa, size: 2048)
public enum Algorithm: Hashable, Sendable, Codable { public enum Algorithm: Hashable, Sendable, Codable {
case ecdsa case ecdsa
case mldsa case mldsa
@ -41,7 +47,7 @@ public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible {
public var algorithm: Algorithm public var algorithm: Algorithm
public var size: Int public var size: Int
public init(algorithm: Algorithm, size: Int) { public init(algorithm: Algorithm, size: Int) {
self.algorithm = algorithm self.algorithm = algorithm
self.size = size self.size = size

View File

@ -46,8 +46,9 @@ public protocol SecretStoreModifiable<SecretType>: SecretStore {
/// Creates a new ``Secret`` in the store. /// Creates a new ``Secret`` in the store.
/// - Parameters: /// - Parameters:
/// - name: The user-facing name for the ``Secret``. /// - name: The user-facing name for the ``Secret``.
/// - attributes: A struct describing the options for creating the key. /// - attributes: A struct describing the options for creating the key.'
func create(name: String, attributes: Attributes) async throws @discardableResult
func create(name: String, attributes: Attributes) async throws -> SecretType
/// Deletes a Secret in the store. /// Deletes a Secret in the store.
/// - Parameters: /// - Parameters:

View File

@ -66,15 +66,15 @@ extension SecureEnclave {
} }
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData) let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
switch (attributes.keyType.algorithm, attributes.keyType.size) { switch attributes.keyType {
case (.ecdsa, 256): case .ecdsa256:
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData, authenticationContext: context) let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData, authenticationContext: context)
return try key.signature(for: data).rawRepresentation return try key.signature(for: data).rawRepresentation
case (.mldsa, 65): case .mldsa65:
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData) let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)
return try key.signature(for: data) return try key.signature(for: data)
case (.mldsa, 87): case .mldsa87:
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData) let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData)
return try key.signature(for: data) return try key.signature(for: data)
@ -98,7 +98,7 @@ extension SecureEnclave {
// MARK: SecretStoreModifiable // MARK: SecretStoreModifiable
public func create(name: String, attributes: Attributes) async throws { public func create(name: String, attributes: Attributes) async throws -> Secret {
var accessError: SecurityError? var accessError: SecurityError?
let flags: SecAccessControlCreateFlags = switch attributes.authentication { let flags: SecAccessControlCreateFlags = switch attributes.authentication {
case .notRequired: case .notRequired:
@ -119,23 +119,28 @@ extension SecureEnclave {
throw error.takeRetainedValue() as Error throw error.takeRetainedValue() as Error
} }
let dataRep: Data let dataRep: Data
switch (attributes.keyType.algorithm, attributes.keyType.size) { let publicKey: Data
case (.ecdsa, 256): switch attributes.keyType {
case .ecdsa256:
let created = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(accessControl: access!) let created = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(accessControl: access!)
dataRep = created.dataRepresentation dataRep = created.dataRepresentation
case (.mldsa, 65): publicKey = created.publicKey.x963Representation
case .mldsa65:
guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() } guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() }
let created = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(accessControl: access!) let created = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(accessControl: access!)
dataRep = created.dataRepresentation dataRep = created.dataRepresentation
case (.mldsa, 87): publicKey = created.publicKey.rawRepresentation
case .mldsa87:
guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() } guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() }
let created = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(accessControl: access!) let created = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(accessControl: access!)
dataRep = created.dataRepresentation dataRep = created.dataRepresentation
publicKey = created.publicKey.rawRepresentation
default: default:
throw Attributes.UnsupportedOptionError() throw Attributes.UnsupportedOptionError()
} }
try saveKey(dataRep, name: name, attributes: attributes) let id = try saveKey(dataRep, name: name, attributes: attributes)
await reloadSecrets() await reloadSecrets()
return Secret(id: id, name: name, publicKey: publicKey, attributes: attributes)
} }
public func delete(secret: Secret) async throws { public func delete(secret: Secret) async throws {
@ -172,11 +177,15 @@ extension SecureEnclave {
} }
public var supportedKeyTypes: [KeyType] { public var supportedKeyTypes: [KeyType] {
[ if #available(macOS 26, *) {
.init(algorithm: .ecdsa, size: 256), [
.init(algorithm: .mldsa, size: 65), .ecdsa256,
.init(algorithm: .mldsa, size: 87), .mldsa65,
] .mldsa87,
]
} else {
[.ecdsa256]
}
} }
} }
@ -220,15 +229,15 @@ extension SecureEnclave.Store {
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData) let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
let keyData = $0[kSecValueData] as! Data let keyData = $0[kSecValueData] as! Data
let publicKey: Data let publicKey: Data
switch (attributes.keyType.algorithm, attributes.keyType.size) { switch attributes.keyType {
case (.ecdsa, 256): case .ecdsa256:
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData) let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData)
publicKey = key.publicKey.x963Representation publicKey = key.publicKey.x963Representation
case (.mldsa, 65): case .mldsa65:
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData) let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)
publicKey = key.publicKey.rawRepresentation publicKey = key.publicKey.rawRepresentation
case (.mldsa, 87): case .mldsa87:
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData) let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData)
publicKey = key.publicKey.rawRepresentation publicKey = key.publicKey.rawRepresentation
@ -249,14 +258,16 @@ extension SecureEnclave.Store {
/// - name: A user-facing name for the key. /// - name: A user-facing name for the key.
/// - attributes: Attributes of the key. /// - attributes: Attributes of the key.
/// - Note: Despite the name, the "Data" of the key is _not_ actual key material. This is an opaque data representation that the SEP can manipulate. /// - Note: Despite the name, the "Data" of the key is _not_ actual key material. This is an opaque data representation that the SEP can manipulate.
func saveKey(_ key: Data, name: String, attributes: Attributes) throws { @discardableResult
func saveKey(_ key: Data, name: String, attributes: Attributes) throws -> String {
let attributes = try JSONEncoder().encode(attributes) let attributes = try JSONEncoder().encode(attributes)
let id = UUID().uuidString
let keychainAttributes = KeychainDictionary([ let keychainAttributes = KeychainDictionary([
kSecClass: Constants.keyClass, kSecClass: Constants.keyClass,
kSecAttrService: Constants.keyTag, kSecAttrService: Constants.keyTag,
kSecUseDataProtectionKeychain: true, kSecUseDataProtectionKeychain: true,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAttrAccount: UUID().uuidString, kSecAttrAccount: id,
kSecValueData: key, kSecValueData: key,
kSecAttrLabel: name, kSecAttrLabel: name,
kSecAttrGeneric: attributes kSecAttrGeneric: attributes
@ -265,6 +276,7 @@ extension SecureEnclave.Store {
if status != errSecSuccess { if status != errSecSuccess {
throw KeychainError(statusCode: status) throw KeychainError(statusCode: status)
} }
return id
} }
} }

View File

@ -61,13 +61,17 @@ extension Preview {
var name: String { "Modifiable Preview Store" } var name: String { "Modifiable Preview Store" }
let secrets: [Secret] let secrets: [Secret]
var supportedKeyTypes: [KeyType] { var supportedKeyTypes: [KeyType] {
[ if #available(macOS 26, *) {
.init(algorithm: .ecdsa, size: 256), [
.init(algorithm: .mldsa, size: 65), .ecdsa256,
.init(algorithm: .mldsa, size: 87), .mldsa65,
] .mldsa87,
]
} else {
[.ecdsa256]
}
} }
init(secrets: [Secret]) { init(secrets: [Secret]) {
self.secrets = secrets self.secrets = secrets
} }