diff --git a/SecretKit/Common/Erasers/AnySecretStore.swift b/SecretKit/Common/Erasers/AnySecretStore.swift index 99f88cb..8f35e4d 100644 --- a/SecretKit/Common/Erasers/AnySecretStore.swift +++ b/SecretKit/Common/Erasers/AnySecretStore.swift @@ -49,10 +49,12 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable { private let _create: (String, Bool) throws -> Void private let _delete: (AnySecret) throws -> Void + private let _update: (AnySecret, String) throws -> Void public init(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { _create = { try secretStore.create(name: $0, requiresAuthentication: $1) } _delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) } + _update = { try secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) } super.init(secretStore) } @@ -64,4 +66,7 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable { try _delete(secret) } + public func update(secret: AnySecret, name: String) throws { + try _update(secret, name) + } } diff --git a/SecretKit/Common/Types/SecretStore.swift b/SecretKit/Common/Types/SecretStore.swift index 2ab51aa..ac88693 100644 --- a/SecretKit/Common/Types/SecretStore.swift +++ b/SecretKit/Common/Types/SecretStore.swift @@ -17,6 +17,7 @@ public protocol SecretStoreModifiable: SecretStore { func create(name: String, requiresAuthentication: Bool) throws func delete(secret: SecretType) throws + func update(secret: SecretType, name: String) throws } diff --git a/SecretKit/SecureEnclave/SecureEnclaveStore.swift b/SecretKit/SecureEnclave/SecureEnclaveStore.swift index c355553..2e42783 100644 --- a/SecretKit/SecureEnclave/SecureEnclaveStore.swift +++ b/SecretKit/SecureEnclave/SecureEnclaveStore.swift @@ -68,7 +68,7 @@ extension SecureEnclave { let deleteAttributes = [ kSecClass: kSecClassKey, kSecAttrApplicationLabel: secret.id as CFData - ] as CFDictionary + ] as CFDictionary let status = SecItemDelete(deleteAttributes) if status != errSecSuccess { throw KeychainError(statusCode: status) @@ -76,6 +76,23 @@ extension SecureEnclave { reloadSecrets() } + public func update(secret: Secret, name: String) throws { + let updateQuery = [ + kSecClass: kSecClassKey, + kSecAttrApplicationLabel: secret.id as CFData + ] as CFDictionary + + let updatedAttributes = [ + kSecAttrLabel: name, + ] as CFDictionary + + let status = SecItemUpdate(updateQuery, updatedAttributes) + if status != errSecSuccess { + throw KeychainError(statusCode: status) + } + reloadSecrets() + } + public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data { let context = LAContext() context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\"" diff --git a/Secretive.xcodeproj/project.pbxproj b/Secretive.xcodeproj/project.pbxproj index 70a3fbf..c458f90 100644 --- a/Secretive.xcodeproj/project.pbxproj +++ b/Secretive.xcodeproj/project.pbxproj @@ -7,9 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */; }; 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; }; 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; - 50153E22250DECA300525160 /* SecretListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListView.swift */; }; + 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 501B7AE1251C56F700776EC7 /* SigningRequestProvenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */; }; 50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */; }; @@ -222,9 +223,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameSecretView.swift; sourceTree = ""; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; - 50153E21250DECA300525160 /* SecretListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListView.swift; sourceTree = ""; }; + 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = ""; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSHWriterTests.swift; sourceTree = ""; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = ""; }; @@ -541,10 +543,11 @@ children = ( 50617D8423FCE48E0099B055 /* ContentView.swift */, 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, - 50153E21250DECA300525160 /* SecretListView.swift */, + 50153E21250DECA300525160 /* SecretListItemView.swift */, 50C385A42407A76D00AF2719 /* SecretDetailView.swift */, 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */, 50B8550C24138C4F009958AC /* DeleteSecretView.swift */, + 2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */, 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, 506772C82425BB8500034DED /* NoStoresView.swift */, 50153E1F250AFCB200525160 /* UpdateView.swift */, @@ -1005,6 +1008,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, @@ -1022,7 +1026,7 @@ 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */, - 50153E22250DECA300525160 /* SecretListView.swift in Sources */, + 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, 508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */, ); diff --git a/Secretive/Preview Content/PreviewStore.swift b/Secretive/Preview Content/PreviewStore.swift index 40b79b9..dc5e6cb 100644 --- a/Secretive/Preview Content/PreviewStore.swift +++ b/Secretive/Preview Content/PreviewStore.swift @@ -42,7 +42,6 @@ extension Preview { } class StoreModifiable: Store, SecretStoreModifiable { - override var name: String { "Modifiable Preview Store" } func create(name: String, requiresAuthentication: Bool) throws { @@ -50,8 +49,10 @@ extension Preview { func delete(secret: Preview.Secret) throws { } - } + func update(secret: Preview.Secret, name: String) throws { + } + } } extension Preview { diff --git a/Secretive/Views/DeleteSecretView.swift b/Secretive/Views/DeleteSecretView.swift index 7810c3e..d9610d8 100644 --- a/Secretive/Views/DeleteSecretView.swift +++ b/Secretive/Views/DeleteSecretView.swift @@ -30,9 +30,6 @@ struct DeleteSecretView: View { TextField(secret.name, text: $confirm) } } - .onExitCommand { - dismissalBlock(false) - } } HStack { Spacer() @@ -47,6 +44,9 @@ struct DeleteSecretView: View { } .padding() .frame(minWidth: 400) + .onExitCommand { + dismissalBlock(false) + } } func delete() { diff --git a/Secretive/Views/RenameSecretView.swift b/Secretive/Views/RenameSecretView.swift new file mode 100644 index 0000000..92977ec --- /dev/null +++ b/Secretive/Views/RenameSecretView.swift @@ -0,0 +1,50 @@ +import SwiftUI +import SecretKit + +struct RenameSecretView: View { + + @ObservedObject var store: StoreType + let secret: StoreType.SecretType + var dismissalBlock: (_ renamed: Bool) -> () + + @State private var newName = "" + + var body: some View { + VStack { + HStack { + Image(nsImage: NSApp.applicationIconImage) + .resizable() + .frame(width: 64, height: 64) + .padding() + VStack { + HStack { + Text("Type your new name for \"\(secret.name)\" below.") + Spacer() + } + HStack { + TextField(secret.name, text: $newName).focusable() + } + } + } + HStack { + Spacer() + Button("Rename", action: rename) + .disabled(newName.count == 0) + .keyboardShortcut(.return) + Button("Cancel") { + dismissalBlock(false) + }.keyboardShortcut(.cancelAction) + } + } + .padding() + .frame(minWidth: 400) + .onExitCommand { + dismissalBlock(false) + } + } + + func rename() { + try? store.update(secret: secret, name: newName) + dismissalBlock(true) + } +} diff --git a/Secretive/Views/SecretListItemView.swift b/Secretive/Views/SecretListItemView.swift new file mode 100644 index 0000000..22c1f99 --- /dev/null +++ b/Secretive/Views/SecretListItemView.swift @@ -0,0 +1,54 @@ +import SwiftUI +import SecretKit + +struct SecretListItemView: View { + + @ObservedObject var store: AnySecretStore + var secret: AnySecret + @Binding var activeSecret: AnySecret.ID? + + @State var isDeleting: Bool = false + @State var isRenaming: Bool = false + + var deletedSecret: (AnySecret) -> Void + var renamedSecret: (AnySecret) -> Void + + var body: some View { + let showingPopupWrapped = Binding( + get: { isDeleting || isRenaming }, + set: { if $0 == false { isDeleting = false; isRenaming = false } } + ) + + return NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) { + Text(secret.name) + }.contextMenu { + if store is AnySecretStoreModifiable { + Button(action: { isRenaming = true }) { + Text("Rename") + } + Button(action: { isDeleting = true }) { + Text("Delete") + } + } + } + .popover(isPresented: showingPopupWrapped) { + if let modifiable = store as? AnySecretStoreModifiable { + if isDeleting { + DeleteSecretView(store: modifiable, secret: secret) { deleted in + isDeleting = false + if deleted { + deletedSecret(secret) + } + } + } else if isRenaming { + RenameSecretView(store: modifiable, secret: secret) { renamed in + isRenaming = false + if renamed { + renamedSecret(secret) + } + } + } + } + } + } +} diff --git a/Secretive/Views/SecretListView.swift b/Secretive/Views/SecretListView.swift deleted file mode 100644 index 27104d2..0000000 --- a/Secretive/Views/SecretListView.swift +++ /dev/null @@ -1,41 +0,0 @@ -import SwiftUI -import SecretKit - -struct SecretListView: View { - - @ObservedObject var store: AnySecretStore - @Binding var activeSecret: AnySecret.ID? - @Binding var deletingSecret: AnySecret? - - var deletedSecret: (AnySecret) -> Void - - var body: some View { - ForEach(store.secrets) { secret in - NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) { - Text(secret.name) - }.contextMenu { - if store is AnySecretStoreModifiable { - Button(action: { delete(secret: secret) }) { - Text("Delete") - } - } - } - .popover(isPresented: .constant(deletingSecret == secret)) { - if let modifiable = store as? AnySecretStoreModifiable { - DeleteSecretView(store: modifiable, secret: secret) { deleted in - deletingSecret = nil - if deleted { - deletedSecret(AnySecret(secret)) - } - } - } - } - - } - } - - func delete(secret: SecretType) { - deletingSecret = AnySecret(secret) - } - -} diff --git a/Secretive/Views/StoreListView.swift b/Secretive/Views/StoreListView.swift index 7730930..84d3afc 100644 --- a/Secretive/Views/StoreListView.swift +++ b/Secretive/Views/StoreListView.swift @@ -6,10 +6,17 @@ struct StoreListView: View { @Binding var showingCreation: Bool @State private var activeSecret: AnySecret.ID? - @State private var deletingSecret: AnySecret? @EnvironmentObject private var storeList: SecretStoreList + private func secretDeleted(secret: AnySecret) { + activeSecret = nextDefaultSecret + } + + private func secretRenamed(secret: AnySecret) { + activeSecret = nextDefaultSecret + } + var body: some View { NavigationView { List(selection: $activeSecret) { @@ -19,9 +26,15 @@ struct StoreListView: View { if store.secrets.isEmpty { EmptyStoreView(store: store, activeSecret: $activeSecret) } else { - SecretListView(store: store, activeSecret: $activeSecret, deletingSecret: $deletingSecret, deletedSecret: { _ in - activeSecret = nextDefaultSecret - }) + ForEach(store.secrets) { secret in + SecretListItemView( + store: store, + secret: secret, + activeSecret: $activeSecret, + deletedSecret: self.secretDeleted, + renamedSecret: self.secretRenamed + ) + } } } } @@ -33,9 +46,7 @@ struct StoreListView: View { } .frame(minWidth: 100, idealWidth: 240) } - } - } extension StoreListView {