From f2012545efc73f102896cb3da11cd7f7bd716e96 Mon Sep 17 00:00:00 2001 From: Dylan Lundy Date: Tue, 27 Apr 2021 00:28:06 +0930 Subject: [PATCH] Add option to rename secrets --- SecretKit/Common/Erasers/AnySecretStore.swift | 5 ++ SecretKit/Common/Types/SecretStore.swift | 1 + .../SecureEnclave/SecureEnclaveStore.swift | 19 ++++++- Secretive.xcodeproj/project.pbxproj | 4 ++ Secretive/Preview Content/PreviewStore.swift | 5 +- Secretive/Views/RenameSecretView.swift | 55 +++++++++++++++++++ Secretive/Views/SecretListView.swift | 31 +++++++++-- Secretive/Views/StoreListView.swift | 20 ++++++- 8 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 Secretive/Views/RenameSecretView.swift diff --git a/SecretKit/Common/Erasers/AnySecretStore.swift b/SecretKit/Common/Erasers/AnySecretStore.swift index 99f88cb..1c09875 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 _updateName: (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) } + _updateName = { 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 _updateName(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..ebdccc4 100644 --- a/Secretive.xcodeproj/project.pbxproj +++ b/Secretive.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ 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 */; }; @@ -222,6 +223,7 @@ /* 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 = ""; }; @@ -545,6 +547,7 @@ 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 */, 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/RenameSecretView.swift b/Secretive/Views/RenameSecretView.swift new file mode 100644 index 0000000..2720405 --- /dev/null +++ b/Secretive/Views/RenameSecretView.swift @@ -0,0 +1,55 @@ +// +// RenameSecretView.swift +// Secretive +// +// Created by Diesal11 on 26/4/21. +// Copyright © 2021 Max Goedjen. All rights reserved. +// + +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("Save", action: rename) + .disabled(newName.count == 0) + .keyboardShortcut(.return) + Button("Cancel") { + dismissalBlock(false) + }.keyboardShortcut(.cancelAction) + } + } + .padding() + .frame(minWidth: 400) + } + + func rename() { + try? store.update(secret: secret, name: newName) + dismissalBlock(true) + } +} diff --git a/Secretive/Views/SecretListView.swift b/Secretive/Views/SecretListView.swift index 27104d2..db8c568 100644 --- a/Secretive/Views/SecretListView.swift +++ b/Secretive/Views/SecretListView.swift @@ -6,8 +6,12 @@ struct SecretListView: View { @ObservedObject var store: AnySecretStore @Binding var activeSecret: AnySecret.ID? @Binding var deletingSecret: AnySecret? + @Binding var renamingSecret: AnySecret? + + @State var renamingText: String = "" var deletedSecret: (AnySecret) -> Void + var renamedSecret: (AnySecret) -> Void var body: some View { ForEach(store.secrets) { secret in @@ -15,17 +19,29 @@ struct SecretListView: View { Text(secret.name) }.contextMenu { if store is AnySecretStoreModifiable { + Button(action: { rename(secret: secret) }) { + Text("Rename") + } Button(action: { delete(secret: secret) }) { Text("Delete") } } } - .popover(isPresented: .constant(deletingSecret == secret)) { + .popover(isPresented: .constant(deletingSecret == secret || renamingSecret == secret)) { if let modifiable = store as? AnySecretStoreModifiable { - DeleteSecretView(store: modifiable, secret: secret) { deleted in - deletingSecret = nil - if deleted { - deletedSecret(AnySecret(secret)) + if deletingSecret == secret { + DeleteSecretView(store: modifiable, secret: secret) { deleted in + deletingSecret = nil + if deleted { + deletedSecret(secret) + } + } + } else if renamingSecret == secret { + RenameSecretView(store: modifiable, secret: secret) { renamed in + renamingSecret = nil + if renamed { + renamedSecret(secret) + } } } } @@ -34,8 +50,11 @@ struct SecretListView: View { } } + func rename(secret: SecretType) { + renamingSecret = AnySecret(secret) + } + func delete(secret: SecretType) { deletingSecret = AnySecret(secret) } - } diff --git a/Secretive/Views/StoreListView.swift b/Secretive/Views/StoreListView.swift index 7730930..d148614 100644 --- a/Secretive/Views/StoreListView.swift +++ b/Secretive/Views/StoreListView.swift @@ -7,10 +7,19 @@ struct StoreListView: View { @State private var activeSecret: AnySecret.ID? @State private var deletingSecret: AnySecret? + @State private var renamingSecret: AnySecret? @EnvironmentObject private var storeList: SecretStoreList var body: some View { + let secretDeleted = { (secret: AnySecret) in + activeSecret = nextDefaultSecret + } + + let secretRenamed = { (secret: AnySecret) in + activeSecret = nextDefaultSecret + } + NavigationView { List(selection: $activeSecret) { ForEach(storeList.stores) { store in @@ -19,9 +28,14 @@ struct StoreListView: View { if store.secrets.isEmpty { EmptyStoreView(store: store, activeSecret: $activeSecret) } else { - SecretListView(store: store, activeSecret: $activeSecret, deletingSecret: $deletingSecret, deletedSecret: { _ in - activeSecret = nextDefaultSecret - }) + SecretListView( + store: store, + activeSecret: $activeSecret, + deletingSecret: $deletingSecret, + renamingSecret: $renamingSecret, + deletedSecret: secretDeleted, + renamedSecret: secretRenamed + ) } } }