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 ![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. 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 }