mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-06 09:47:22 +01:00
CryptoKit migration (#628)
* WIP. * WIP * WIP Edit * Key selection. * WIP * WIP * Proxy through * WIP * Remove verify. * Migration. * Comment * Add param * Semi-offering key * Ignore updates if test build. * Fix rsa public key gen * Messily fix RSA * Remove 1024 bit rsa * Cleanup * Cleanup * Clean out MLDSA refs for now * Dump notifier changes * Put back UI tweaks * Fixes.
This commit is contained in:
@@ -30,7 +30,7 @@ struct ContentView: View {
|
||||
}
|
||||
.frame(minWidth: 640, minHeight: 320)
|
||||
.toolbar {
|
||||
// toolbarItem(updateNoticeView, id: "update")
|
||||
toolbarItem(updateNoticeView, id: "update")
|
||||
toolbarItem(runningOrRunSetupView, id: "setup")
|
||||
toolbarItem(appPathNoticeView, id: "appPath")
|
||||
toolbarItem(newItemView, id: "new")
|
||||
|
||||
@@ -7,244 +7,123 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
@Binding var showing: Bool
|
||||
|
||||
@State private var name = ""
|
||||
@State private var requiresAuthentication = true
|
||||
@State private var keyAttribution = ""
|
||||
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
|
||||
@State private var keyType: KeyType?
|
||||
@State var advanced = false
|
||||
|
||||
private var authenticationOptions: [AuthenticationRequirement] {
|
||||
if advanced || authenticationRequirement == .biometryCurrent {
|
||||
[.presenceRequired, .notRequired, .biometryCurrent]
|
||||
} else {
|
||||
[.presenceRequired, .notRequired]
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(.createSecretTitle)
|
||||
.font(.largeTitle)
|
||||
Spacer()
|
||||
VStack(alignment: .trailing) {
|
||||
Form {
|
||||
Section {
|
||||
TextField(String(localized: .createSecretNameLabel), text: $name, prompt: Text(.createSecretNamePlaceholder))
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Picker(.createSecretRequireAuthenticationTitle, selection: $authenticationRequirement) {
|
||||
ForEach(authenticationOptions) { option in
|
||||
HStack {
|
||||
switch option {
|
||||
case .notRequired:
|
||||
Image(systemName: "bell")
|
||||
Text(.createSecretNotifyTitle)
|
||||
case .presenceRequired:
|
||||
Image(systemName: "lock")
|
||||
Text(.createSecretRequireAuthenticationTitle)
|
||||
case .biometryCurrent:
|
||||
Image(systemName: "lock.trianglebadge.exclamationmark.fill")
|
||||
Text("Current Biometrics")
|
||||
case .unknown:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.tag(option)
|
||||
}
|
||||
}
|
||||
Group {
|
||||
switch authenticationRequirement {
|
||||
case .notRequired:
|
||||
Text(.createSecretNotifyDescription)
|
||||
case .presenceRequired:
|
||||
Text(.createSecretRequireAuthenticationDescription)
|
||||
case .biometryCurrent:
|
||||
Text("Require authentication with current set of biometrics.")
|
||||
case .unknown:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
if authenticationRequirement == .biometryCurrent {
|
||||
Text("If you change your biometric settings in _any way_, including adding a new fingerprint, this key will no longer be accessible.")
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 3)
|
||||
.background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5))
|
||||
}
|
||||
|
||||
}
|
||||
HStack {
|
||||
Text(.createSecretNameLabel)
|
||||
TextField(String(localized: .createSecretNamePlaceholder), text: $name)
|
||||
.focusable()
|
||||
}
|
||||
if advanced {
|
||||
Section {
|
||||
VStack {
|
||||
Picker("Key Type", selection: $keyType) {
|
||||
ForEach(store.supportedKeyTypes, id: \.self) { option in
|
||||
Text(String(describing: option))
|
||||
.tag(option)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
TextField("Key Attribution", text: $keyAttribution, prompt: Text("test@example.com"))
|
||||
Text("This shows at the end of your public key.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
ThumbnailPickerView(items: [
|
||||
ThumbnailPickerView.Item(value: true, name: .createSecretRequireAuthenticationTitle, description: .createSecretRequireAuthenticationDescription, thumbnail: AuthenticationView()),
|
||||
ThumbnailPickerView.Item(value: false, name: .createSecretNotifyTitle,
|
||||
description: .createSecretNotifyDescription,
|
||||
thumbnail: NotificationView())
|
||||
], selection: $requiresAuthentication)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Toggle("Advanced", isOn: $advanced)
|
||||
.toggleStyle(.button)
|
||||
Spacer()
|
||||
Button(.createSecretCancelButton) {
|
||||
Button(.createSecretCancelButton, role: .cancel) {
|
||||
showing = false
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
Button(.createSecretCreateButton, action: save)
|
||||
.disabled(name.isEmpty)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
}.padding()
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
keyType = store.supportedKeyTypes.first
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
|
||||
func save() {
|
||||
let attribution = keyAttribution.isEmpty ? nil : keyAttribution
|
||||
Task {
|
||||
try! await store.create(name: name, requiresAuthentication: requiresAuthentication)
|
||||
try! await store.create(
|
||||
name: name,
|
||||
attributes: .init(
|
||||
keyType: keyType!,
|
||||
authentication: authenticationRequirement,
|
||||
publicKeyAttribution: attribution
|
||||
)
|
||||
)
|
||||
showing = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ThumbnailPickerView<ValueType: Hashable>: View {
|
||||
|
||||
private let items: [Item<ValueType>]
|
||||
@Binding var selection: ValueType
|
||||
|
||||
init(items: [ThumbnailPickerView<ValueType>.Item<ValueType>], selection: Binding<ValueType>) {
|
||||
self.items = items
|
||||
_selection = selection
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
ForEach(items) { item in
|
||||
VStack(alignment: .leading, spacing: 15) {
|
||||
item.thumbnail
|
||||
.frame(height: 200)
|
||||
.overlay(RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(lineWidth: item.value == selection ? 15 : 0))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
.foregroundColor(.accentColor)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(item.name)
|
||||
.bold()
|
||||
Text(item.description)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.frame(width: 250)
|
||||
.onTapGesture {
|
||||
withAnimation(.spring()) {
|
||||
selection = item.value
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
|
||||
}
|
||||
|
||||
extension ThumbnailPickerView {
|
||||
|
||||
struct Item<InnerValueType: Hashable>: Identifiable {
|
||||
let id = UUID()
|
||||
let value: InnerValueType
|
||||
let name: LocalizedStringResource
|
||||
let description: LocalizedStringResource
|
||||
let thumbnail: AnyView
|
||||
|
||||
init<ViewType: View>(value: InnerValueType, name: LocalizedStringResource, description: LocalizedStringResource, thumbnail: ViewType) {
|
||||
self.value = value
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.thumbnail = AnyView(thumbnail)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@MainActor @Observable class SystemBackground {
|
||||
|
||||
static let shared = SystemBackground()
|
||||
var image: NSImage?
|
||||
|
||||
private init() {
|
||||
if let mainScreen = NSScreen.main, let imageURL = NSWorkspace.shared.desktopImageURL(for: mainScreen) {
|
||||
image = NSImage(contentsOf: imageURL)
|
||||
} else {
|
||||
image = nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SystemBackgroundView: View {
|
||||
|
||||
let anchor: UnitPoint
|
||||
|
||||
var body: some View {
|
||||
if let image = SystemBackground.shared.image {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.scaleEffect(3, anchor: anchor)
|
||||
.clipped()
|
||||
.allowsHitTesting(false)
|
||||
} else {
|
||||
Rectangle()
|
||||
.foregroundColor(Color(.systemPurple))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
SystemBackgroundView(anchor: .center)
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
Image(systemName: "touchid")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(Color(.systemRed))
|
||||
Text(verbatim: "Touch ID Prompt")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
.redacted(reason: .placeholder)
|
||||
VStack {
|
||||
Text(verbatim: "Touch ID Detail prompt.Detail two.")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.primary)
|
||||
Text(verbatim: "Touch ID Detail prompt.Detail two.")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.redacted(reason: .placeholder)
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.frame(width: geometry.size.width, height: 20, alignment: .center)
|
||||
.foregroundColor(.accentColor)
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.frame(width: geometry.size.width, height: 20, alignment: .center)
|
||||
.foregroundColor(Color(.unemphasizedSelectedContentBackgroundColor))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 150)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(.ultraThickMaterial)
|
||||
)
|
||||
.padding()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct NotificationView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
SystemBackgroundView(anchor: .topTrailing)
|
||||
VStack {
|
||||
Rectangle()
|
||||
.background(Color.clear)
|
||||
.foregroundStyle(.thinMaterial)
|
||||
.frame(height: 35)
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Image(nsImage: NSApplication.shared.applicationIconImage)
|
||||
.resizable()
|
||||
.frame(width: 64, height: 64)
|
||||
.foregroundColor(.primary)
|
||||
VStack(alignment: .leading) {
|
||||
Text(verbatim: "Secretive")
|
||||
.font(.title)
|
||||
.foregroundColor(.primary)
|
||||
Text(verbatim: "Secretive wants to sign")
|
||||
.font(.body)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}.padding()
|
||||
.redacted(reason: .placeholder)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(.ultraThickMaterial)
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct CreateSecretView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
|
||||
AuthenticationView().environment(\.colorScheme, .dark)
|
||||
AuthenticationView().environment(\.colorScheme, .light)
|
||||
NotificationView().environment(\.colorScheme, .dark)
|
||||
NotificationView().environment(\.colorScheme, .light)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
57
Sources/Secretive/Views/EditSecretView.swift
Normal file
57
Sources/Secretive/Views/EditSecretView.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
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))
|
||||
VStack(alignment: .leading) {
|
||||
TextField("Key Attribution", text: $publicKeyAttribution, prompt: Text("test@example.com"))
|
||||
Text("This shows at the end of your public key.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
|
||||
let secret: SecretType
|
||||
|
||||
private let keyWriter = OpenSSHKeyWriter()
|
||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID))
|
||||
|
||||
var body: some View {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ struct SecretListItemView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(value: secret) {
|
||||
if secret.requiresAuthentication {
|
||||
if secret.authenticationRequirement.required {
|
||||
HStack {
|
||||
Text(secret.name)
|
||||
Spacer()
|
||||
@@ -39,14 +39,16 @@ struct SecretListItemView: View {
|
||||
.contextMenu {
|
||||
if store is AnySecretStoreModifiable {
|
||||
Button(action: { isRenaming = true }) {
|
||||
Text(.secretListRenameButton)
|
||||
Image(systemName: "pencil")
|
||||
Text(.secretListEditButton)
|
||||
}
|
||||
Button(action: { isDeleting = true }) {
|
||||
Image(systemName: "trash")
|
||||
Text(.secretListDeleteButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
.popover(isPresented: showingPopup) {
|
||||
.sheet(isPresented: showingPopup) {
|
||||
if let modifiable = store as? AnySecretStoreModifiable {
|
||||
if isDeleting {
|
||||
DeleteSecretView(store: modifiable, secret: secret) { deleted in
|
||||
@@ -56,7 +58,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)
|
||||
|
||||
Reference in New Issue
Block a user