mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-08-19 03:30:56 +00:00
WIP Edit
This commit is contained in:
parent
276ca02b39
commit
7aba3c374d
@ -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 {
|
||||
|
@ -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] {
|
||||
|
@ -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: " ")
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 */,
|
||||
|
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
52
Sources/Secretive/Views/EditSecretView.swift
Normal file
52
Sources/Secretive/Views/EditSecretView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user