mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-08-30 17:10:56 +00:00
Merge branch 'main' into filehandle_session
This commit is contained in:
commit
04ca953fca
126
APP_CONFIG.md
126
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`
|
||||
|
||||
```
|
||||
<?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.
|
||||
Instructions for setting up apps and shells has moved to [secretive-config-instructions](https://github.com/maxgoedjen/secretive-config-instructions)!
|
||||
|
2
FAQ.md
2
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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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<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) }
|
||||
_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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)])
|
||||
|
@ -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
|
||||
|
@ -46,8 +46,9 @@ public protocol SecretStoreModifiable<SecretType>: 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:
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user