This commit is contained in:
Max Goedjen 2025-08-17 23:51:05 -05:00
parent 276ca02b39
commit 7aba3c374d
No known key found for this signature in database
17 changed files with 129 additions and 125 deletions

View File

@ -7,10 +7,8 @@ public struct AnySecret: Secret, @unchecked Sendable {
private let hashable: AnyHashable
private let _id: () -> AnyHashable
private let _name: () -> String
private let _keyType: () -> KeyType
private let _authenticationRequirement: () -> AuthenticationRequirement
private let _publicKey: () -> Data
private let _publicKeyAttribution: () -> String?
private let _attributes: () -> Attributes
public init<T>(_ secret: T) where T: Secret {
if let secret = secret as? AnySecret {
@ -18,19 +16,15 @@ public struct AnySecret: Secret, @unchecked Sendable {
hashable = secret.hashable
_id = secret._id
_name = secret._name
_keyType = secret._keyType
_authenticationRequirement = secret._authenticationRequirement
_publicKey = secret._publicKey
_publicKeyAttribution = secret._publicKeyAttribution
_attributes = secret._attributes
} else {
base = secret as Any
self.hashable = secret
_id = { secret.id as AnyHashable }
_name = { secret.name }
_keyType = { secret.keyType }
_authenticationRequirement = { secret.authenticationRequirement }
_publicKey = { secret.publicKey }
_publicKeyAttribution = { secret.publicKeyAttribution }
_attributes = { secret.attributes }
}
}
@ -42,21 +36,12 @@ public struct AnySecret: Secret, @unchecked Sendable {
_name()
}
public var keyType: KeyType {
_keyType()
}
public var authenticationRequirement: AuthenticationRequirement {
_authenticationRequirement()
}
public var publicKey: Data {
_publicKey()
}
public var publicKeyAttribution: String? {
_publicKeyAttribution()
public var attributes: Attributes {
_attributes()
}
public static func == (lhs: AnySecret, rhs: AnySecret) -> Bool {

View File

@ -70,13 +70,13 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
private let _create: @Sendable (String, Attributes) async throws -> Void
private let _delete: @Sendable (AnySecret) async throws -> Void
private let _update: @Sendable (AnySecret, String) async throws -> Void
private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
private let _supportedKeyTypes: @Sendable () -> [KeyType]
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { try await secretStore.create(name: $0, attributes: $1) }
_delete = { try await secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
_update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) }
_update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1, attributes: $2) }
_supportedKeyTypes = { secretStore.supportedKeyTypes }
super.init(secretStore)
}
@ -89,8 +89,8 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
try await _delete(secret)
}
public func update(secret: AnySecret, name: String) async throws {
try await _update(secret, name)
public func update(secret: AnySecret, name: String, attributes: Attributes) async throws {
try await _update(secret, name, attributes)
}
public var supportedKeyTypes: [KeyType] {

View File

@ -18,8 +18,19 @@ public struct OpenSSHKeyWriter: Sendable {
/// Generates an OpenSSH string representation of the secret.
/// - Returns: OpenSSH string representation of the secret.
public func openSSHString<SecretType: Secret>(secret: SecretType, comment: String? = nil) -> String {
[curveType(for: secret.keyType), data(secret: secret).base64EncodedString(), comment]
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String {
let resolvedComment: String
if let comment = secret.publicKeyAttribution {
resolvedComment = comment
} else {
let dashedKeyName = secret.name.replacingOccurrences(of: " ", with: "-")
let dashedHostName = ["secretive", Host.current().localizedName, "local"]
.compactMap { $0 }
.joined(separator: ".")
.replacingOccurrences(of: " ", with: "-")
resolvedComment = "\(dashedKeyName)@\(dashedHostName)"
}
return [curveType(for: secret.keyType), data(secret: secret).base64EncodedString(), resolvedComment]
.compactMap { $0 }
.joined(separator: " ")
}

View File

@ -1,10 +1,10 @@
import Foundation
public struct Attributes: Sendable, Codable {
public struct Attributes: Sendable, Codable, Hashable {
/// The type of key involved.
public var keyType: KeyType
public let keyType: KeyType
/// The authentication requirements for the key. This is simply a description of the option recorded at creation modifying it doers not modify the key's authentication requirements.
public let authentication: AuthenticationRequirement

View File

@ -5,14 +5,28 @@ public protocol Secret: Identifiable, Hashable, Sendable {
/// A user-facing string identifying the Secret.
var name: String { get }
/// The algorithm this secret uses.
var keyType: KeyType { get }
/// Whether the secret requires authentication before use.
var authenticationRequirement: AuthenticationRequirement { get }
/// The public key data for the secret.
var publicKey: Data { get }
/// The attributes of the key.
var attributes: Attributes { get }
}
public extension Secret {
/// The algorithm and key size this secret uses.
var keyType: KeyType {
attributes.keyType
}
/// Whether the secret requires authentication before use.
var authenticationRequirement: AuthenticationRequirement {
attributes.authentication
}
/// An attribution string to apply to the generated public key.
var publicKeyAttribution: String? { get }
var publicKeyAttribution: String? {
attributes.publicKeyAttribution
}
}

View File

@ -67,8 +67,9 @@ public protocol SecretStoreModifiable: SecretStore {
/// - Parameters:
/// - secret: The ``Secret`` to update.
/// - name: The new name for the Secret.
func update(secret: SecretType, name: String) async throws
/// - attributes: The new attributes for the secret.
func update(secret: SecretType, name: String, attributes: Attributes) async throws
var supportedKeyTypes: [KeyType] { get }
}

View File

@ -188,7 +188,7 @@ extension SecureEnclave {
await reloadSecretsInternal()
}
public func update(secret: Secret, name: String) async throws {
public func update(secret: Secret, name: String, attributes: Attributes) async throws {
let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData

View File

@ -9,10 +9,8 @@ extension SecureEnclave {
public let id: Data
public let name: String
public let keyType: KeyType
public let authenticationRequirement: AuthenticationRequirement
public let publicKeyAttribution: String?
public let publicKey: Data
public let attributes: Attributes
init(
id: Data,
@ -22,10 +20,15 @@ extension SecureEnclave {
) {
self.id = id
self.name = name
self.keyType = .init(algorithm: .ecdsa, size: 256)
self.authenticationRequirement = authenticationRequirement
self.publicKeyAttribution = nil
self.publicKey = publicKey
self.attributes = Attributes(
keyType: .init(
algorithm: .ecdsa,
size: 256
),
authentication: authenticationRequirement,
publicKeyAttribution: nil
)
}
init(
@ -36,10 +39,8 @@ extension SecureEnclave {
) {
self.id = Data(id.utf8)
self.name = name
self.keyType = attributes.keyType
self.authenticationRequirement = attributes.authentication
self.publicKeyAttribution = attributes.publicKeyAttribution
self.publicKey = publicKey
self.attributes = attributes
}
}

View File

@ -174,7 +174,7 @@ extension SecureEnclave {
await reloadSecretsInternal()
}
public func update(secret: Secret, name: String) async throws {
public func update(secret: Secret, name: String, attributes: Attributes) async throws {
let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData

View File

@ -9,10 +9,8 @@ extension SmartCard {
public let id: Data
public let name: String
public let keyType: KeyType
public let authenticationRequirement: AuthenticationRequirement = .unknown
public let publicKey: Data
public var publicKeyAttribution: String? = nil
public var attributes: Attributes
}

View File

@ -180,7 +180,8 @@ extension SmartCard.Store {
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
let publicKey = publicKeyAttributes[kSecValueData] as! Data
return SmartCard.Secret(id: tokenID, name: name, keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, publicKey: publicKey)
let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!)
return SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes)
}
state.secrets.append(contentsOf: wrapped)
}

View File

@ -7,7 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */; };
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; };
50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; };
50033AC327813F1700253856 /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; };
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; };
@ -98,7 +98,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameSecretView.swift; sourceTree = "<group>"; };
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = "<group>"; };
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
@ -246,7 +246,7 @@
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
@ -430,7 +430,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */,
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,

View File

@ -9,10 +9,13 @@ extension Preview {
let id = UUID().uuidString
let name: String
let keyType = KeyType(algorithm: .ecdsa, size: 256)
let authenticationRequirement = AuthenticationRequirement.presenceRequired
let publicKey = UUID().uuidString.data(using: .utf8)!
var publicKeyAttribution: String?
var attributes: Attributes {
Attributes(
keyType: .init(algorithm: .ecdsa, size: 256),
authentication: .presenceRequired,
)
}
}
}
@ -99,7 +102,7 @@ extension Preview {
func delete(secret: Preview.Secret) throws {
}
func update(secret: Preview.Secret, name: String) throws {
func update(secret: Preview.Secret, name: String, attributes: Attributes) throws {
}
}
}

View File

@ -0,0 +1,52 @@
import SwiftUI
import SecretKit
struct EditSecretView<StoreType: SecretStoreModifiable>: View {
let store: StoreType
let secret: StoreType.SecretType
let dismissalBlock: (_ renamed: Bool) -> ()
@State private var name: String
@State private var publicKeyAttribution: String
init(store: StoreType, secret: StoreType.SecretType, dismissalBlock: @escaping (Bool) -> ()) {
self.store = store
self.secret = secret
self.dismissalBlock = dismissalBlock
name = secret.name
publicKeyAttribution = secret.publicKeyAttribution ?? ""
}
var body: some View {
VStack(alignment: .trailing) {
Form {
Section {
TextField(String(localized: .createSecretNameLabel), text: $name, prompt: Text(.createSecretNamePlaceholder))
TextField("Key Attribution", text: $publicKeyAttribution, prompt: Text("test@example.com"))
}
}
HStack {
Button(.renameRenameButton, action: rename)
.disabled(name.isEmpty)
.keyboardShortcut(.return)
Button(.renameCancelButton) {
dismissalBlock(false)
}.keyboardShortcut(.cancelAction)
}
.padding()
}
.formStyle(.grouped)
}
func rename() {
var attributes = secret.attributes
if !publicKeyAttribution.isEmpty {
attributes.publicKeyAttribution = publicKeyAttribution
}
Task {
try? await store.update(secret: secret, name: name, attributes: attributes)
dismissalBlock(true)
}
}
}

View File

@ -1,52 +0,0 @@
import SwiftUI
import SecretKit
struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
@State var store: StoreType
let secret: StoreType.SecretType
var dismissalBlock: (_ renamed: Bool) -> ()
@State private var newName = ""
var body: some View {
VStack {
HStack {
Image(nsImage: NSApplication.shared.applicationIconImage)
.resizable()
.frame(width: 64, height: 64)
.padding()
VStack {
HStack {
Text(.renameTitle(secretName: secret.name))
Spacer()
}
HStack {
TextField(secret.name, text: $newName).focusable()
}
}
}
HStack {
Spacer()
Button(.renameRenameButton, action: rename)
.disabled(newName.count == 0)
.keyboardShortcut(.return)
Button(.renameCancelButton) {
dismissalBlock(false)
}.keyboardShortcut(.cancelAction)
}
}
.padding()
.frame(minWidth: 400)
.onExitCommand {
dismissalBlock(false)
}
}
func rename() {
Task {
try? await store.update(secret: secret, name: newName)
dismissalBlock(true)
}
}
}

View File

@ -30,19 +30,9 @@ struct SecretDetailView<SecretType: Secret>: View {
.frame(minHeight: 200, maxHeight: .infinity)
}
var dashedKeyName: String {
secret.name.replacingOccurrences(of: " ", with: "-")
}
var dashedHostName: String {
["secretive", Host.current().localizedName, "local"]
.compactMap { $0 }
.joined(separator: ".")
.replacingOccurrences(of: " ", with: "-")
}
var keyString: String {
keyWriter.openSSHString(secret: secret, comment: "\(dashedKeyName)@\(dashedHostName)")
keyWriter.openSSHString(secret: secret)
}
}

View File

@ -46,7 +46,7 @@ struct SecretListItemView: View {
}
}
}
.popover(isPresented: showingPopup) {
.sheet(isPresented: showingPopup) {
if let modifiable = store as? AnySecretStoreModifiable {
if isDeleting {
DeleteSecretView(store: modifiable, secret: secret) { deleted in
@ -56,7 +56,7 @@ struct SecretListItemView: View {
}
}
} else if isRenaming {
RenameSecretView(store: modifiable, secret: secret) { renamed in
EditSecretView(store: modifiable, secret: secret) { renamed in
isRenaming = false
if renamed {
renamedSecret(secret)