diff --git a/APP_CONFIG.md b/APP_CONFIG.md
index 5de6319..448d9c5 100644
--- a/APP_CONFIG.md
+++ b/APP_CONFIG.md
@@ -1,125 +1,3 @@
-# Setting up Third Party Apps FAQ
+# App Configuration
-## Tower
-
-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`
-
-```
-
-
-
-
- Label
- link-ssh-auth-sock
- ProgramArguments
-
- /bin/sh
- -c
- /bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK
-
- RunAtLoad
-
-
-
-```
-
-Log out and log in again before launching Cyberduck.
-
-## Mountain Duck
-
-Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
-
-```
-
-
-
-
- Label
- link-ssh-auth-sock
- ProgramArguments
-
- /bin/sh
- -c
- /bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK
-
- RunAtLoad
-
-
-
-```
-
-Log out and log in again before launching Mountain Duck.
-
-## GitKraken
-
-Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
-
-```
-
-
-
-
- Label
- link-ssh-auth-sock
- ProgramArguments
-
- /bin/sh
- -c
- /bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK
-
- RunAtLoad
-
-
-
-```
-
-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.
+Instructions for setting up apps and shells has moved to [secretive-config-instructions](https://github.com/maxgoedjen/secretive-config-instructions)!
diff --git a/FAQ.md b/FAQ.md
index 0145aeb..7c22fdb 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -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 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
diff --git a/README.md b/README.md
index 21b10ea..50d8cd1 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Secretive  
+# Secretive [](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) 
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.
diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift
index cb47e95..9e94618 100644
--- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift
+++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift
@@ -61,20 +61,21 @@ open class AnySecretStore: SecretStore, @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 _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
private let _supportedKeyTypes: @Sendable () -> [KeyType]
public init(_ 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) }
_update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1, attributes: $2) }
_supportedKeyTypes = { secretStore.supportedKeyTypes }
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)
}
diff --git a/Sources/Packages/Sources/SecretKit/KeychainTypes.swift b/Sources/Packages/Sources/SecretKit/KeychainTypes.swift
index debb2e1..9d08c65 100644
--- a/Sources/Packages/Sources/SecretKit/KeychainTypes.swift
+++ b/Sources/Packages/Sources/SecretKit/KeychainTypes.swift
@@ -53,12 +53,12 @@ public extension SecretStore {
/// - secret: The secret which will be used for signing.
/// - Returns: The appropriate algorithm.
func signatureAlgorithm(for secret: SecretType) -> SecKeyAlgorithm? {
- switch (secret.keyType.algorithm, secret.keyType.size) {
- case (.ecdsa, 256):
+ switch secret.keyType {
+ case .ecdsa256:
.ecdsaSignatureMessageX962SHA256
- case (.ecdsa, 384):
+ case .ecdsa384:
.ecdsaSignatureMessageX962SHA384
- case (.rsa, 2048):
+ case .rsa2048:
.rsaSignatureMessagePKCS1v15SHA512
default:
nil
diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift
index d809755..2c7f446 100644
--- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift
+++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift
@@ -75,16 +75,16 @@ extension OpenSSHPublicKeyWriter {
/// - length: The key length of the algorithm.
/// - Returns: The OpenSSH identifier for the algorithm.
public func openSSHIdentifier(for keyType: KeyType) -> String {
- switch (keyType.algorithm, keyType.size) {
- case (.ecdsa, 256):
+ switch keyType {
+ case .ecdsa256:
"ecdsa-sha2-nistp256"
- case (.ecdsa, 384):
+ case .ecdsa384:
"ecdsa-sha2-nistp384"
- case (.mldsa, 65):
+ case .mldsa65:
"ssh-mldsa-65"
- case (.mldsa, 87):
+ case .mldsa87:
"ssh-mldsa-87"
- case (.rsa, _):
+ case .rsa2048:
"ssh-rsa"
default:
"unknown"
@@ -101,8 +101,7 @@ extension OpenSSHPublicKeyWriter {
// [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
// we only support one key type, and the keychain always gives it in a specific format.
- let keySize = secret.keyType.size
- guard secret.keyType.algorithm == .rsa && keySize == 2048 else { fatalError() }
+ guard secret.keyType == .rsa2048 else { fatalError() }
let length = secret.keyType.size/8
let data = secret.publicKey
let n = Data(data[8..<(9+length)])
diff --git a/Sources/Packages/Sources/SecretKit/Types/Secret.swift b/Sources/Packages/Sources/SecretKit/Types/Secret.swift
index 0f74b48..6b952f6 100644
--- a/Sources/Packages/Sources/SecretKit/Types/Secret.swift
+++ b/Sources/Packages/Sources/SecretKit/Types/Secret.swift
@@ -32,7 +32,13 @@ public extension Secret {
/// The type of algorithm the Secret uses.
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 {
case ecdsa
case mldsa
@@ -41,7 +47,7 @@ public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible {
public var algorithm: Algorithm
public var size: Int
-
+
public init(algorithm: Algorithm, size: Int) {
self.algorithm = algorithm
self.size = size
diff --git a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift
index c1dcdec..14abc9f 100644
--- a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift
+++ b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift
@@ -46,8 +46,9 @@ public protocol SecretStoreModifiable: SecretStore {
/// Creates a new ``Secret`` in the store.
/// - Parameters:
/// - name: The user-facing name for the ``Secret``.
- /// - attributes: A struct describing the options for creating the key.
- func create(name: String, attributes: Attributes) async throws
+ /// - attributes: A struct describing the options for creating the key.'
+ @discardableResult
+ func create(name: String, attributes: Attributes) async throws -> SecretType
/// Deletes a Secret in the store.
/// - Parameters:
diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift
index 725ad87..b94dc5e 100644
--- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift
+++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift
@@ -66,15 +66,15 @@ extension SecureEnclave {
}
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
- switch (attributes.keyType.algorithm, attributes.keyType.size) {
- case (.ecdsa, 256):
+ switch attributes.keyType {
+ case .ecdsa256:
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData, authenticationContext: context)
return try key.signature(for: data).rawRepresentation
- case (.mldsa, 65):
+ case .mldsa65:
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)
return try key.signature(for: data)
- case (.mldsa, 87):
+ case .mldsa87:
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData)
return try key.signature(for: data)
@@ -98,7 +98,7 @@ extension SecureEnclave {
// MARK: SecretStoreModifiable
- public func create(name: String, attributes: Attributes) async throws {
+ public func create(name: String, attributes: Attributes) async throws -> Secret {
var accessError: SecurityError?
let flags: SecAccessControlCreateFlags = switch attributes.authentication {
case .notRequired:
@@ -119,23 +119,28 @@ extension SecureEnclave {
throw error.takeRetainedValue() as Error
}
let dataRep: Data
- switch (attributes.keyType.algorithm, attributes.keyType.size) {
- case (.ecdsa, 256):
+ let publicKey: Data
+ switch attributes.keyType {
+ case .ecdsa256:
let created = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(accessControl: access!)
dataRep = created.dataRepresentation
- case (.mldsa, 65):
+ publicKey = created.publicKey.x963Representation
+ case .mldsa65:
guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() }
let created = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(accessControl: access!)
dataRep = created.dataRepresentation
- case (.mldsa, 87):
+ publicKey = created.publicKey.rawRepresentation
+ case .mldsa87:
guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() }
let created = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(accessControl: access!)
dataRep = created.dataRepresentation
+ publicKey = created.publicKey.rawRepresentation
default:
throw Attributes.UnsupportedOptionError()
}
- try saveKey(dataRep, name: name, attributes: attributes)
+ let id = try saveKey(dataRep, name: name, attributes: attributes)
await reloadSecrets()
+ return Secret(id: id, name: name, publicKey: publicKey, attributes: attributes)
}
public func delete(secret: Secret) async throws {
@@ -172,11 +177,15 @@ extension SecureEnclave {
}
public var supportedKeyTypes: [KeyType] {
- [
- .init(algorithm: .ecdsa, size: 256),
- .init(algorithm: .mldsa, size: 65),
- .init(algorithm: .mldsa, size: 87),
- ]
+ if #available(macOS 26, *) {
+ [
+ .ecdsa256,
+ .mldsa65,
+ .mldsa87,
+ ]
+ } else {
+ [.ecdsa256]
+ }
}
}
@@ -220,15 +229,15 @@ extension SecureEnclave.Store {
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
let keyData = $0[kSecValueData] as! Data
let publicKey: Data
- switch (attributes.keyType.algorithm, attributes.keyType.size) {
- case (.ecdsa, 256):
+ switch attributes.keyType {
+ case .ecdsa256:
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData)
publicKey = key.publicKey.x963Representation
- case (.mldsa, 65):
+ case .mldsa65:
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)
publicKey = key.publicKey.rawRepresentation
- case (.mldsa, 87):
+ case .mldsa87:
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData)
publicKey = key.publicKey.rawRepresentation
@@ -249,14 +258,16 @@ extension SecureEnclave.Store {
/// - name: A user-facing name for 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.
- 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 id = UUID().uuidString
let keychainAttributes = KeychainDictionary([
kSecClass: Constants.keyClass,
kSecAttrService: Constants.keyTag,
kSecUseDataProtectionKeychain: true,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
- kSecAttrAccount: UUID().uuidString,
+ kSecAttrAccount: id,
kSecValueData: key,
kSecAttrLabel: name,
kSecAttrGeneric: attributes
@@ -265,6 +276,7 @@ extension SecureEnclave.Store {
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
+ return id
}
}
diff --git a/Sources/Secretive/Preview Content/PreviewStore.swift b/Sources/Secretive/Preview Content/PreviewStore.swift
index ff8f8da..8897023 100644
--- a/Sources/Secretive/Preview Content/PreviewStore.swift
+++ b/Sources/Secretive/Preview Content/PreviewStore.swift
@@ -61,13 +61,17 @@ extension Preview {
var name: String { "Modifiable Preview Store" }
let secrets: [Secret]
var supportedKeyTypes: [KeyType] {
- [
- .init(algorithm: .ecdsa, size: 256),
- .init(algorithm: .mldsa, size: 65),
- .init(algorithm: .mldsa, size: 87),
- ]
+ if #available(macOS 26, *) {
+ [
+ .ecdsa256,
+ .mldsa65,
+ .mldsa87,
+ ]
+ } else {
+ [.ecdsa256]
+ }
}
-
+
init(secrets: [Secret]) {
self.secrets = secrets
}