mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-10 03:07:22 +02:00
Compare commits
7 Commits
newsetup_l
...
extensions
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f1f83113 | ||
|
|
3e128d2a81 | ||
|
|
935ac32ea2 | ||
|
|
a0a632f245 | ||
|
|
51fed9e593 | ||
|
|
f652d1d961 | ||
|
|
8aacd428b1 |
@@ -1,108 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
"" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"agent_details_could_not_start_error" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_details_disable_agent_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Disable Agent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_details_restart_agent_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Restart Agent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_details_running_since_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_details_socket_path_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Socket Path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_details_start_agent_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Start Agent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_details_start_agent_button_starting" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Starting Agent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_details_version_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_not_running_notice_detail_description" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**Secretive will not be able to function properly unless the agent is installed and running.**"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_not_running_notice_title" : {
|
"agent_not_running_notice_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -488,17 +386,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agentDetailsLocationTitle" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Secret Agent Location"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"app_menu_help_button" : {
|
"app_menu_help_button" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1274,17 +1161,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copy_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Copy"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"copyable_click_to_copy_button" : {
|
"copyable_click_to_copy_button" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1621,7 +1497,7 @@
|
|||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "This shows at the end of your public key. It’s usually an email address."
|
"value" : "This shows at the end of your public key."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3132,248 +3008,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"integrations_add_this_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Add This:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_apps_row_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Apps"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_community_apps_list_description" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_community_shell_list_description" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_multiple_config" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "You can configure more than one tool, they generally won't interfere with each other."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_row_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Getting Started"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_section_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Integrations"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_suggestion_git" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "If you're trying to sign your git commits, set up Git Signing."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_suggestion_shell" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "If you're trying to configure anything your command line runs to use Secretive, configure your shell."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_suggestion_shell_default" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "If you don't known what shell you use and haven't changed it, you're probably using `%(shellName)@`."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_suggestion_ssh" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Configuring Tools for Secretive"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_title_description" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_getting_started_what_should_i_configure_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "What Should I Configure?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_other_section_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Other"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_other_shell_row_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "other"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_path_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Configuration File"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_shell_section_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Shell"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_system_section_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "System"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_view_other_github_link" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "View on GitHub"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrations_web_link" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "View Documentation on Web"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"integrationsMenuBarTitle" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Integrations…"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"no_secure_storage_description" : {
|
"no_secure_storage_description" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3761,17 +3395,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reveal_in_finder_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Reveal in Finder"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"secret_detail_md5_fingerprint_label" : {
|
"secret_detail_md5_fingerprint_label" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4269,16 +3892,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Setup" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Setup"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"setup_agent_activity_monitor_description" : {
|
"setup_agent_activity_monitor_description" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4563,50 +4176,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup_done_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Done"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"setup_integrations_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Configure"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"setup_integrations_description" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Tell the tools you use how to talk to Secretive."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"setup_integrations_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Configure Integrations"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"setup_ssh_add_for_me_button" : {
|
"setup_ssh_add_for_me_button" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -5181,7 +4750,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setup_updates_ok_button" : {
|
"setup_updates_ok" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ca" : {
|
"ca" : {
|
||||||
@@ -5394,17 +4963,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setupStepCompleteButton" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Done"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"signed_notification_description" : {
|
"signed_notification_description" : {
|
||||||
"comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the description, showing which secret was used. The placeholder is the name of the secret.",
|
"comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the description, showing which secret was used. The placeholder is the name of the secret.",
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ extension Agent {
|
|||||||
}
|
}
|
||||||
let requestTypeInt = data[4]
|
let requestTypeInt = data[4]
|
||||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription) for unknown request type \(requestTypeInt)")
|
||||||
return SSHAgent.ResponseType.agentFailure.data.lengthAndData
|
return SSHAgent.ResponseType.agentFailure.data.lengthAndData
|
||||||
}
|
}
|
||||||
logger.debug("Agent handling request of type \(requestType.debugDescription)")
|
logger.debug("Agent handling request of type \(requestType.debugDescription)")
|
||||||
@@ -66,10 +66,25 @@ extension Agent {
|
|||||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||||
response.append(try await sign(data: data, provenance: provenance))
|
response.append(try await sign(data: data, provenance: provenance))
|
||||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
||||||
|
case .protocolExtension:
|
||||||
|
response.append(SSHAgent.ResponseType.agentExtensionResponse.data)
|
||||||
|
try await handleExtension(data)
|
||||||
|
default:
|
||||||
|
let reader = OpenSSHReader(data: data)
|
||||||
|
while true {
|
||||||
|
do {
|
||||||
|
let payloadHash = try reader.readNextChunk()
|
||||||
|
print(String(String(decoding: payloadHash, as: UTF8.self)))
|
||||||
|
print(payloadHash)
|
||||||
|
} catch {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("Agent received valid request of type \(requestType.debugDescription), but not currently supported.")
|
||||||
|
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
response.removeAll()
|
response = SSHAgent.ResponseType.agentFailure.data
|
||||||
response.append(SSHAgent.ResponseType.agentFailure.data)
|
|
||||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||||
}
|
}
|
||||||
return response.lengthAndData
|
return response.lengthAndData
|
||||||
@@ -77,6 +92,28 @@ extension Agent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PROTOCOL EXTENSIONS
|
||||||
|
extension Agent {
|
||||||
|
|
||||||
|
func handleExtension(_ data: Data) async throws {
|
||||||
|
let reader = OpenSSHReader(data: data)
|
||||||
|
guard try reader.readNextChunkAsString() == "session-bind@openssh.com" else { throw UnsupportedExtensionError() }
|
||||||
|
let hostKey = try reader.readNextChunk()
|
||||||
|
let keyReader = OpenSSHReader(data: hostKey)
|
||||||
|
_ = try keyReader.readNextChunkAsString() // Key Type
|
||||||
|
let keyData = try keyReader.readNextChunk()
|
||||||
|
let sessionID = try reader.readNextChunk()
|
||||||
|
let signatureData = try reader.readNextChunk()
|
||||||
|
let forwarding = try reader.readNextBytes(as: Bool.self)
|
||||||
|
let signatureReader = OpenSSHSignatureReader()
|
||||||
|
guard try signatureReader.verify(signatureData, for: sessionID, with: keyData) else { throw SignatureVerificationFailedError() }
|
||||||
|
print("Fowarding: \(forwarding)")
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UnsupportedExtensionError: Error {}
|
||||||
|
struct SignatureVerificationFailedError: Error {}
|
||||||
|
}
|
||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
/// Lists the identities available for signing operations
|
/// Lists the identities available for signing operations
|
||||||
@@ -89,9 +126,8 @@ extension Agent {
|
|||||||
|
|
||||||
for secret in secrets {
|
for secret in secrets {
|
||||||
let keyBlob = publicKeyWriter.data(secret: secret)
|
let keyBlob = publicKeyWriter.data(secret: secret)
|
||||||
let curveData = publicKeyWriter.openSSHIdentifier(for: secret.keyType)
|
|
||||||
keyData.append(keyBlob.lengthAndData)
|
keyData.append(keyBlob.lengthAndData)
|
||||||
keyData.append(curveData.lengthAndData)
|
keyData.append(publicKeyWriter.comment(secret: secret).lengthAndData)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) {
|
if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) {
|
||||||
@@ -113,7 +149,7 @@ extension Agent {
|
|||||||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||||
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||||
let reader = OpenSSHReader(data: data)
|
let reader = OpenSSHReader(data: data)
|
||||||
let payloadHash = reader.readNextChunk()
|
let payloadHash = try reader.readNextChunk()
|
||||||
let hash: Data
|
let hash: Data
|
||||||
|
|
||||||
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
|
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
|
||||||
@@ -130,7 +166,7 @@ extension Agent {
|
|||||||
|
|
||||||
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
||||||
|
|
||||||
let dataToSign = reader.readNextChunk()
|
let dataToSign = try reader.readNextChunk()
|
||||||
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
|
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
|
||||||
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,32 @@ extension SSHAgent {
|
|||||||
|
|
||||||
case requestIdentities = 11
|
case requestIdentities = 11
|
||||||
case signRequest = 13
|
case signRequest = 13
|
||||||
|
case addIdentity = 17
|
||||||
|
case removeIdentity = 18
|
||||||
|
case removeAllIdentities = 19
|
||||||
|
case addIDConstrained = 25
|
||||||
|
case addSmartcardKey = 20
|
||||||
|
case removeSmartcardKey = 21
|
||||||
|
case lock = 22
|
||||||
|
case unlock = 23
|
||||||
|
case addSmartcardKeyConstrained = 26
|
||||||
|
case protocolExtension = 27
|
||||||
|
|
||||||
|
|
||||||
public var debugDescription: String {
|
public var debugDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .requestIdentities:
|
case .requestIdentities: "SSH_AGENTC_REQUEST_IDENTITIES"
|
||||||
return "RequestIdentities"
|
case .signRequest: "SSH_AGENTC_SIGN_REQUEST"
|
||||||
case .signRequest:
|
case .addIdentity: "SSH_AGENTC_ADD_IDENTITY"
|
||||||
return "SignRequest"
|
case .removeIdentity: "SSH_AGENTC_REMOVE_IDENTITY"
|
||||||
|
case .removeAllIdentities: "SSH_AGENTC_REMOVE_ALL_IDENTITIES"
|
||||||
|
case .addIDConstrained: "SSH_AGENTC_ADD_ID_CONSTRAINED"
|
||||||
|
case .addSmartcardKey: "SSH_AGENTC_ADD_SMARTCARD_KEY"
|
||||||
|
case .removeSmartcardKey: "SSH_AGENTC_REMOVE_SMARTCARD_KEY"
|
||||||
|
case .lock: "SSH_AGENTC_LOCK"
|
||||||
|
case .unlock: "SSH_AGENTC_UNLOCK"
|
||||||
|
case .addSmartcardKeyConstrained: "SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED"
|
||||||
|
case .protocolExtension: "SSH_AGENTC_EXTENSION"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,17 +47,17 @@ extension SSHAgent {
|
|||||||
case agentSuccess = 6
|
case agentSuccess = 6
|
||||||
case agentIdentitiesAnswer = 12
|
case agentIdentitiesAnswer = 12
|
||||||
case agentSignResponse = 14
|
case agentSignResponse = 14
|
||||||
|
case agentExtensionFailure = 28
|
||||||
|
case agentExtensionResponse = 29
|
||||||
|
|
||||||
public var debugDescription: String {
|
public var debugDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .agentFailure:
|
case .agentFailure: "SSH_AGENT_FAILURE"
|
||||||
return "AgentFailure"
|
case .agentSuccess: "SSH_AGENT_SUCCESS"
|
||||||
case .agentSuccess:
|
case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER"
|
||||||
return "AgentSuccess"
|
case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE"
|
||||||
case .agentIdentitiesAnswer:
|
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
|
||||||
return "AgentIdentitiesAnswer"
|
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
|
||||||
case .agentSignResponse:
|
|
||||||
return "AgentSignResponse"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ extension SocketController {
|
|||||||
provenance = SigningRequestTracer().provenance(from: fileHandle)
|
provenance = SigningRequestTracer().provenance(from: fileHandle)
|
||||||
(messages, messagesContinuation) = AsyncStream.makeStream()
|
(messages, messagesContinuation) = AsyncStream.makeStream()
|
||||||
Task { [messagesContinuation, logger] in
|
Task { [messagesContinuation, logger] in
|
||||||
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
|
|
||||||
for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) {
|
for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) {
|
||||||
let data = fileHandle.availableData
|
let data = fileHandle.availableData
|
||||||
guard !data.isEmpty else {
|
guard !data.isEmpty else {
|
||||||
@@ -91,6 +90,9 @@ extension SocketController {
|
|||||||
logger.debug("Socket controller yielded data.")
|
logger.debug("Socket controller yielded data.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Task {
|
||||||
|
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes new data to the socket.
|
/// Writes new data to the socket.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import OSLog
|
|||||||
/// Manages storage and lookup for OpenSSH certificates.
|
/// Manages storage and lookup for OpenSSH certificates.
|
||||||
public actor OpenSSHCertificateHandler: Sendable {
|
public actor OpenSSHCertificateHandler: Sendable {
|
||||||
|
|
||||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
|
||||||
private let writer = OpenSSHPublicKeyWriter()
|
private let writer = OpenSSHPublicKeyWriter()
|
||||||
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
|
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
|
||||||
@@ -30,20 +30,24 @@ public actor OpenSSHCertificateHandler: Sendable {
|
|||||||
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
|
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
|
||||||
public func publicKeyHash(from hash: Data) -> Data? {
|
public func publicKeyHash(from hash: Data) -> Data? {
|
||||||
let reader = OpenSSHReader(data: hash)
|
let reader = OpenSSHReader(data: hash)
|
||||||
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
|
do {
|
||||||
switch certType {
|
let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self)
|
||||||
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
switch certType {
|
||||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||||
_ = reader.readNextChunk() // nonce
|
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
||||||
let curveIdentifier = reader.readNextChunk()
|
_ = try reader.readNextChunk() // nonce
|
||||||
let publicKey = reader.readNextChunk()
|
let curveIdentifier = try reader.readNextChunk()
|
||||||
|
let publicKey = try reader.readNextChunk()
|
||||||
|
|
||||||
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||||
return openSSHIdentifier.lengthAndData +
|
return openSSHIdentifier.lengthAndData +
|
||||||
curveIdentifier.lengthAndData +
|
curveIdentifier.lengthAndData +
|
||||||
publicKey.lengthAndData
|
publicKey.lengthAndData
|
||||||
default:
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,18 +31,7 @@ public struct OpenSSHPublicKeyWriter: Sendable {
|
|||||||
/// Generates an OpenSSH string representation of the secret.
|
/// Generates an OpenSSH string representation of the secret.
|
||||||
/// - Returns: OpenSSH string representation of the secret.
|
/// - Returns: OpenSSH string representation of the secret.
|
||||||
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String {
|
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String {
|
||||||
let resolvedComment: String
|
return [openSSHIdentifier(for: secret.keyType), data(secret: secret).base64EncodedString(), comment(secret: secret)]
|
||||||
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 [openSSHIdentifier(for: secret.keyType), data(secret: secret).base64EncodedString(), resolvedComment]
|
|
||||||
.compactMap { $0 }
|
.compactMap { $0 }
|
||||||
.joined(separator: " ")
|
.joined(separator: " ")
|
||||||
}
|
}
|
||||||
@@ -65,6 +54,19 @@ public struct OpenSSHPublicKeyWriter: Sendable {
|
|||||||
.joined(separator: ":")
|
.joined(separator: ":")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func comment<SecretType: Secret>(secret: SecretType) -> String {
|
||||||
|
if let comment = secret.publicKeyAttribution {
|
||||||
|
return comment
|
||||||
|
} else {
|
||||||
|
let dashedKeyName = secret.name.replacingOccurrences(of: " ", with: "-")
|
||||||
|
let dashedHostName = ["secretive", Host.current().localizedName, "local"]
|
||||||
|
.compactMap { $0 }
|
||||||
|
.joined(separator: ".")
|
||||||
|
.replacingOccurrences(of: " ", with: "-")
|
||||||
|
return "\(dashedKeyName)@\(dashedHostName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension OpenSSHPublicKeyWriter {
|
extension OpenSSHPublicKeyWriter {
|
||||||
@@ -95,7 +97,7 @@ extension OpenSSHPublicKeyWriter {
|
|||||||
|
|
||||||
extension OpenSSHPublicKeyWriter {
|
extension OpenSSHPublicKeyWriter {
|
||||||
|
|
||||||
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
|
func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
|
||||||
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
|
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
|
||||||
// Keychain stores it as a thin ASN.1 wrapper with this format:
|
// Keychain stores it as a thin ASN.1 wrapper with this format:
|
||||||
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e]
|
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e]
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ public final class OpenSSHReader {
|
|||||||
|
|
||||||
/// Reads the next chunk of data from the playload.
|
/// Reads the next chunk of data from the playload.
|
||||||
/// - Returns: The next chunk of data.
|
/// - Returns: The next chunk of data.
|
||||||
public func readNextChunk() -> Data {
|
public func readNextChunk() throws -> Data {
|
||||||
|
guard remaining.count > UInt32.bitWidth/8 else { throw EndOfData() }
|
||||||
let lengthRange = 0..<(UInt32.bitWidth/8)
|
let lengthRange = 0..<(UInt32.bitWidth/8)
|
||||||
let lengthChunk = remaining[lengthRange]
|
let lengthChunk = remaining[lengthRange]
|
||||||
remaining.removeSubrange(lengthRange)
|
remaining.removeSubrange(lengthRange)
|
||||||
@@ -25,4 +26,18 @@ public final class OpenSSHReader {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func readNextBytes<T>(as: T.Type) throws -> T {
|
||||||
|
let lengthRange = 0..<MemoryLayout<T>.size
|
||||||
|
let lengthChunk = remaining[lengthRange]
|
||||||
|
remaining.removeSubrange(lengthRange)
|
||||||
|
return lengthChunk.bytes.unsafeLoad(as: T.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func readNextChunkAsString() throws -> String {
|
||||||
|
try String(decoding: readNextChunk(), as: UTF8.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EndOfData: Error {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
import Security
|
||||||
|
|
||||||
|
/// Reads OpenSSH representations of Secrets.
|
||||||
|
public struct OpenSSHSignatureReader: Sendable {
|
||||||
|
|
||||||
|
/// Initializes the reader.
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func verify(_ signatureData: Data, for signedData: Data, with publicKey: Data) throws -> Bool {
|
||||||
|
let reader = OpenSSHReader(data: signatureData)
|
||||||
|
let signatureType = try reader.readNextChunkAsString()
|
||||||
|
let signatureData = try reader.readNextChunk()
|
||||||
|
switch signatureType {
|
||||||
|
case "ssh-rsa":
|
||||||
|
let attributes = KeychainDictionary([
|
||||||
|
kSecAttrKeyType: kSecAttrKeyTypeRSA,
|
||||||
|
kSecAttrKeySizeInBits: 2048,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
||||||
|
])
|
||||||
|
var verifyError: SecurityError?
|
||||||
|
let untyped: CFTypeRef? = SecKeyCreateWithData(publicKey as CFData, attributes, &verifyError)
|
||||||
|
guard let untypedSafe = untyped else {
|
||||||
|
throw KeychainError(statusCode: errSecSuccess)
|
||||||
|
}
|
||||||
|
let key = untypedSafe as! SecKey
|
||||||
|
return SecKeyVerifySignature(key, .rsaSignatureMessagePKCS1v15SHA512, signedData as CFData, signatureData as CFData, nil)
|
||||||
|
case "ecdsa-sha2-nistp256":
|
||||||
|
return try P256.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
|
||||||
|
case "ecdsa-sha2-nistp384":
|
||||||
|
return try P384.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
|
||||||
|
case "ecdsa-sha2-nistp521":
|
||||||
|
return try P521.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData)
|
||||||
|
case "ssh-ed25519":
|
||||||
|
return try Curve25519.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
|
||||||
|
case "ssh-mldsa-65":
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
return try MLDSA65.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
|
||||||
|
} else {
|
||||||
|
throw UnsupportedSignatureType()
|
||||||
|
}
|
||||||
|
case "ssh-mldsa-87":
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
return try MLDSA87.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData)
|
||||||
|
} else {
|
||||||
|
throw UnsupportedSignatureType()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw UnsupportedSignatureType()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct UnsupportedSignatureType: Error {}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@ import OSLog
|
|||||||
public final class PublicKeyFileStoreController: Sendable {
|
public final class PublicKeyFileStoreController: Sendable {
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
|
||||||
private let directory: URL
|
private let directory: String
|
||||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||||
|
|
||||||
/// Initializes a PublicKeyFileStoreController.
|
/// Initializes a PublicKeyFileStoreController.
|
||||||
public init(homeDirectory: URL) {
|
public init(homeDirectory: String) {
|
||||||
directory = homeDirectory.appending(component: "PublicKeys")
|
directory = homeDirectory.appending("/PublicKeys")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes out the keys specified to disk.
|
/// Writes out the keys specified to disk.
|
||||||
@@ -20,7 +20,7 @@ public final class PublicKeyFileStoreController: Sendable {
|
|||||||
logger.log("Writing public keys to disk")
|
logger.log("Writing public keys to disk")
|
||||||
if clear {
|
if clear {
|
||||||
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
||||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
|
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []
|
||||||
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
|
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
|
||||||
|
|
||||||
let untracked = Set(fullPathContents)
|
let untracked = Set(fullPathContents)
|
||||||
@@ -29,7 +29,7 @@ public final class PublicKeyFileStoreController: Sendable {
|
|||||||
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
|
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
|
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
|
||||||
for secret in secrets {
|
for secret in secrets {
|
||||||
let path = publicKeyPath(for: secret)
|
let path = publicKeyPath(for: secret)
|
||||||
let data = Data(keyWriter.openSSHString(secret: secret).utf8)
|
let data = Data(keyWriter.openSSHString(secret: secret).utf8)
|
||||||
@@ -44,14 +44,14 @@ public final class PublicKeyFileStoreController: Sendable {
|
|||||||
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
|
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
|
||||||
public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String {
|
public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||||
return directory.appending(component: "\(minimalHex).pub").path()
|
return directory.appending("/").appending("\(minimalHex).pub")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
|
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
|
||||||
public var hasAnyCertificates: Bool {
|
public var hasAnyCertificates: Bool {
|
||||||
do {
|
do {
|
||||||
return try FileManager.default
|
return try FileManager.default
|
||||||
.contentsOfDirectory(atPath: directory.path())
|
.contentsOfDirectory(atPath: directory)
|
||||||
.filter { $0.hasSuffix("-cert.pub") }
|
.filter { $0.hasSuffix("-cert.pub") }
|
||||||
.isEmpty == false
|
.isEmpty == false
|
||||||
} catch {
|
} catch {
|
||||||
@@ -65,7 +65,7 @@ public final class PublicKeyFileStoreController: Sendable {
|
|||||||
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
|
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
|
||||||
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||||
return directory.appending(component: "\(minimalHex)-cert.pub").path()
|
return directory.appending("/").appending("\(minimalHex)-cert.pub")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}()
|
}()
|
||||||
private let updater = Updater(checkOnLaunch: true)
|
private let updater = Updater(checkOnLaunch: true)
|
||||||
private let notifier = Notifier()
|
private let notifier = Notifier()
|
||||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||||
private lazy var agent: Agent = {
|
private lazy var agent: Agent = {
|
||||||
Agent(storeList: storeList, witness: notifier)
|
Agent(storeList: storeList, witness: notifier)
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
||||||
|
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
|
||||||
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
|
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
|
||||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
|
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
|
||||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
|
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
|
||||||
@@ -48,12 +49,8 @@
|
|||||||
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
|
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
|
||||||
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
|
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
|
||||||
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
|
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
|
||||||
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */; };
|
|
||||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
|
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
|
||||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
|
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
|
||||||
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
|
|
||||||
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; };
|
|
||||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
|
|
||||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
||||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -123,6 +120,7 @@
|
|||||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
|
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
|
||||||
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
||||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
|
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
|
||||||
|
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
|
||||||
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
||||||
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
|
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
|
||||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
|
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
|
||||||
@@ -140,12 +138,8 @@
|
|||||||
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; };
|
50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; };
|
||||||
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
|
|
||||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
|
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
|
||||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
|
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
|
||||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = "<group>"; };
|
|
||||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = "<group>"; };
|
|
||||||
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
|
|
||||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
||||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
|
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@@ -260,11 +254,7 @@
|
|||||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||||
5066A6C12516F303004B5A36 /* SetupView.swift */,
|
5066A6C12516F303004B5A36 /* SetupView.swift */,
|
||||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
|
||||||
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
|
|
||||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
|
||||||
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
|
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -276,6 +266,7 @@
|
|||||||
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
|
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
|
||||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
||||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
|
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
|
||||||
|
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
|
||||||
);
|
);
|
||||||
path = Controllers;
|
path = Controllers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -442,7 +433,6 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
|
|
||||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
||||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||||
@@ -452,18 +442,16 @@
|
|||||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
||||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
||||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
|
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
|
||||||
|
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */,
|
||||||
50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
|
50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
|
||||||
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */,
|
|
||||||
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
|
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
|
||||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
|
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
|
||||||
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
|
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
|
||||||
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
|
|
||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
|
||||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
|
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
|
||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
||||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
|
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
|
||||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
|
|
||||||
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
||||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
||||||
@@ -659,18 +647,10 @@
|
|||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
|
||||||
INFOPLIST_FILE = Secretive/Info.plist;
|
INFOPLIST_FILE = Secretive/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -699,18 +679,10 @@
|
|||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
|
||||||
INFOPLIST_FILE = Secretive/Info.plist;
|
INFOPLIST_FILE = Secretive/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -811,18 +783,10 @@
|
|||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
|
||||||
INFOPLIST_FILE = Secretive/Info.plist;
|
INFOPLIST_FILE = Secretive/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -845,17 +809,8 @@
|
|||||||
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
|
||||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -880,17 +835,8 @@
|
|||||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
|
||||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -916,17 +862,8 @@
|
|||||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
|
||||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ struct Secretive: App {
|
|||||||
@Environment(\.agentStatusChecker) var agentStatusChecker
|
@Environment(\.agentStatusChecker) var agentStatusChecker
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||||
@State private var showingSetup = false
|
@State private var showingSetup = false
|
||||||
@State private var showingIntegrations = false
|
|
||||||
@State private var showingCreation = false
|
@State private var showingCreation = false
|
||||||
|
|
||||||
@SceneBuilder var body: some Scene {
|
@SceneBuilder var body: some Scene {
|
||||||
@@ -59,16 +58,8 @@ struct Secretive: App {
|
|||||||
forceLaunchAgent()
|
forceLaunchAgent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingIntegrations) {
|
|
||||||
IntegrationsView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.commands {
|
.commands {
|
||||||
CommandGroup(before: CommandGroupPlacement.appSettings) {
|
|
||||||
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
|
|
||||||
showingIntegrations = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CommandGroup(after: CommandGroupPlacement.newItem) {
|
CommandGroup(after: CommandGroupPlacement.newItem) {
|
||||||
Button(.appMenuNewSecretButton) {
|
Button(.appMenuNewSecretButton) {
|
||||||
showingCreation = true
|
showingCreation = true
|
||||||
@@ -81,7 +72,7 @@ struct Secretive: App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
CommandGroup(after: .help) {
|
CommandGroup(after: .help) {
|
||||||
Button("Setup") {
|
Button(.appMenuSetupButton) {
|
||||||
showingSetup = true
|
showingSetup = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +87,7 @@ extension Secretive {
|
|||||||
private func reinstallAgent() {
|
private func reinstallAgent() {
|
||||||
justUpdatedChecker.check()
|
justUpdatedChecker.check()
|
||||||
Task {
|
Task {
|
||||||
_ = await LaunchAgentController().install()
|
await LaunchAgentController().install()
|
||||||
try? await Task.sleep(for: .seconds(1))
|
try? await Task.sleep(for: .seconds(1))
|
||||||
agentStatusChecker.check()
|
agentStatusChecker.check()
|
||||||
if !agentStatusChecker.running {
|
if !agentStatusChecker.running {
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import Observation
|
|||||||
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
|
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
|
||||||
var running: Bool { get }
|
var running: Bool { get }
|
||||||
var developmentBuild: Bool { get }
|
var developmentBuild: Bool { get }
|
||||||
var process: NSRunningApplication? { get }
|
|
||||||
func check()
|
func check()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
|
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
|
||||||
|
|
||||||
var running: Bool = false
|
var running: Bool = false
|
||||||
var process: NSRunningApplication? = nil
|
|
||||||
|
|
||||||
nonisolated init() {
|
nonisolated init() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
@@ -22,39 +20,32 @@ import Observation
|
|||||||
}
|
}
|
||||||
|
|
||||||
func check() {
|
func check() {
|
||||||
process = instanceSecretAgentProcess
|
running = instanceSecretAgentProcess != nil
|
||||||
running = process != nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All processes, including ones from older versions, etc
|
// All processes, including ones from older versions, etc
|
||||||
var allSecretAgentProcesses: [NSRunningApplication] {
|
var secretAgentProcesses: [NSRunningApplication] {
|
||||||
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.agentBundleID)
|
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The process corresponding to this instance of Secretive
|
// The process corresponding to this instance of Secretive
|
||||||
var instanceSecretAgentProcess: NSRunningApplication? {
|
var instanceSecretAgentProcess: NSRunningApplication? {
|
||||||
// FIXME: CHECK VERSION
|
let agents = secretAgentProcesses
|
||||||
let agents = allSecretAgentProcesses
|
|
||||||
for agent in agents {
|
for agent in agents {
|
||||||
guard let url = agent.bundleURL else { continue }
|
guard let url = agent.bundleURL else { continue }
|
||||||
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) {
|
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) {
|
||||||
return agent
|
return agent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Whether Secretive is being run in an Xcode environment.
|
// Whether Secretive is being run in an Xcode environment.
|
||||||
var developmentBuild: Bool {
|
var developmentBuild: Bool {
|
||||||
Bundle.main.bundleURL.isXcodeURL
|
Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension URL {
|
|
||||||
|
|
||||||
var isXcodeURL: Bool {
|
|
||||||
absoluteString.contains("/Library/Developer/Xcode")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,28 +8,16 @@ struct LaunchAgentController {
|
|||||||
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
|
||||||
|
|
||||||
func install() async -> Bool {
|
func install() async {
|
||||||
logger.debug("Installing agent")
|
logger.debug("Installing agent")
|
||||||
_ = setEnabled(false)
|
_ = setEnabled(false)
|
||||||
// This is definitely a bit of a "seems to work better" thing but:
|
// This is definitely a bit of a "seems to work better" thing but:
|
||||||
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
|
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
|
||||||
// and start new?
|
// and start new?
|
||||||
try? await Task.sleep(for: .seconds(1))
|
try? await Task.sleep(for: .seconds(1))
|
||||||
let result = await MainActor.run {
|
await MainActor.run {
|
||||||
setEnabled(true)
|
_ = setEnabled(true)
|
||||||
}
|
}
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func uninstall() async -> Bool {
|
|
||||||
logger.debug("Uninstalling agent")
|
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
let result = await MainActor.run {
|
|
||||||
setEnabled(false)
|
|
||||||
}
|
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func forceLaunch() async -> Bool {
|
func forceLaunch() async -> Bool {
|
||||||
@@ -40,7 +28,6 @@ struct LaunchAgentController {
|
|||||||
do {
|
do {
|
||||||
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
|
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
|
||||||
logger.debug("Agent force launched")
|
logger.debug("Agent force launched")
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Error force launching \(error.localizedDescription)")
|
logger.error("Error force launching \(error.localizedDescription)")
|
||||||
@@ -49,7 +36,7 @@ struct LaunchAgentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setEnabled(_ enabled: Bool) -> Bool {
|
private func setEnabled(_ enabled: Bool) -> Bool {
|
||||||
let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
|
let service = SMAppService.loginItem(identifier: Bundle.main.agentBundleID)
|
||||||
do {
|
do {
|
||||||
if enabled {
|
if enabled {
|
||||||
try service.register()
|
try service.register()
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import Foundation
|
||||||
|
import Cocoa
|
||||||
|
import SecretKit
|
||||||
|
|
||||||
|
struct ShellConfigurationController {
|
||||||
|
|
||||||
|
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
||||||
|
|
||||||
|
var shellInstructions: [ShellConfigInstruction] {
|
||||||
|
[
|
||||||
|
ShellConfigInstruction(shell: "global",
|
||||||
|
shellConfigDirectory: "~/.ssh/",
|
||||||
|
shellConfigFilename: "config",
|
||||||
|
text: "Host *\n\tIdentityAgent \(socketPath)"),
|
||||||
|
ShellConfigInstruction(shell: "zsh",
|
||||||
|
shellConfigDirectory: "~/",
|
||||||
|
shellConfigFilename: ".zshrc",
|
||||||
|
text: "export SSH_AUTH_SOCK=\(socketPath)"),
|
||||||
|
ShellConfigInstruction(shell: "bash",
|
||||||
|
shellConfigDirectory: "~/",
|
||||||
|
shellConfigFilename: ".bashrc",
|
||||||
|
text: "export SSH_AUTH_SOCK=\(socketPath)"),
|
||||||
|
ShellConfigInstruction(shell: "fish",
|
||||||
|
shellConfigDirectory: "~/.config/fish",
|
||||||
|
shellConfigFilename: "config.fish",
|
||||||
|
text: "set -x SSH_AUTH_SOCK \(socketPath)"),
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@MainActor func addToShell(shellInstructions: ShellConfigInstruction) -> Bool {
|
||||||
|
let openPanel = NSOpenPanel()
|
||||||
|
// This is sync, so no need to strongly retain
|
||||||
|
let delegate = Delegate(name: shellInstructions.shellConfigFilename)
|
||||||
|
openPanel.delegate = delegate
|
||||||
|
openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically."
|
||||||
|
openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)"
|
||||||
|
openPanel.canChooseFiles = true
|
||||||
|
openPanel.canChooseDirectories = false
|
||||||
|
openPanel.showsHiddenFiles = true
|
||||||
|
openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory)
|
||||||
|
openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename
|
||||||
|
openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText]
|
||||||
|
openPanel.runModal()
|
||||||
|
guard let fileURL = openPanel.urls.first else { return false }
|
||||||
|
let handle: FileHandle
|
||||||
|
do {
|
||||||
|
handle = try FileHandle(forUpdating: fileURL)
|
||||||
|
guard let existing = try handle.readToEnd(),
|
||||||
|
let existingString = String(data: existing, encoding: .utf8) else { return false }
|
||||||
|
guard !existingString.contains(shellInstructions.text) else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
try handle.seekToEnd()
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
handle.write(Data("\n# Secretive Config\n\(shellInstructions.text)\n".utf8))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Bundle {
|
|
||||||
public static var agentBundleID: String {
|
|
||||||
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent")
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var hostBundleID: String {
|
extension Bundle {
|
||||||
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host")
|
public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!}
|
||||||
}
|
public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
|
||||||
|
|
||||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||||
|
|
||||||
let running: Bool
|
let running: Bool
|
||||||
let process: NSRunningApplication?
|
|
||||||
let developmentBuild = false
|
let developmentBuild = false
|
||||||
|
|
||||||
init(running: Bool = true, process: NSRunningApplication? = nil) {
|
init(running: Bool = true) {
|
||||||
self.running = running
|
self.running = running
|
||||||
self.process = process
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func check() {
|
func check() {
|
||||||
|
|||||||
@@ -2,84 +2,14 @@ import SwiftUI
|
|||||||
|
|
||||||
struct PrimaryButtonModifier: ViewModifier {
|
struct PrimaryButtonModifier: ViewModifier {
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
@Environment(\.isEnabled) var isEnabled
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
// Tinted glass prominent is really hard to read on 26.0.
|
|
||||||
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
|
|
||||||
content.buttonStyle(.glassProminent)
|
|
||||||
} else {
|
|
||||||
content.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
func primaryButton() -> some View {
|
|
||||||
modifier(PrimaryButtonModifier())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MenuButtonModifier: ViewModifier {
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if #available(macOS 26.0, *) {
|
|
||||||
content
|
|
||||||
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
func menuButton() -> some View {
|
|
||||||
modifier(MenuButtonModifier())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NormalButtonModifier: ViewModifier {
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if #available(macOS 26.0, *) {
|
|
||||||
content.buttonStyle(.glass)
|
|
||||||
} else {
|
|
||||||
content.buttonStyle(.bordered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
func normalButton() -> some View {
|
|
||||||
modifier(NormalButtonModifier())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DangerButtonModifier: ViewModifier {
|
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
// Tinted glass prominent is really hard to read on 26.0.
|
// Tinted glass prominent is really hard to read on 26.0.
|
||||||
if #available(macOS 26.0, *), colorScheme == .dark {
|
if #available(macOS 26.0, *), colorScheme == .dark {
|
||||||
content.buttonStyle(.glassProminent)
|
content.buttonStyle(.glassProminent)
|
||||||
.tint(.red)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
} else {
|
} else {
|
||||||
content.buttonStyle(.borderedProminent)
|
content.buttonStyle(.borderedProminent)
|
||||||
.tint(.red)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +17,8 @@ struct DangerButtonModifier: ViewModifier {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
func danger() -> some View {
|
func primary() -> some View {
|
||||||
modifier(DangerButtonModifier())
|
modifier(PrimaryButtonModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct AgentStatusView: View {
|
|
||||||
|
|
||||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
if agentStatusChecker.running {
|
|
||||||
AgentRunningView()
|
|
||||||
} else {
|
|
||||||
AgentNotRunningView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
struct AgentRunningView: View {
|
|
||||||
|
|
||||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
|
||||||
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
Section {
|
|
||||||
if let process = agentStatusChecker.process {
|
|
||||||
ConfigurationItemView(
|
|
||||||
title: .agentDetailsLocationTitle,
|
|
||||||
value: process.bundleURL!.path(),
|
|
||||||
action: .revealInFinder(process.bundleURL!.path()),
|
|
||||||
)
|
|
||||||
ConfigurationItemView(
|
|
||||||
title: .agentDetailsSocketPathTitle,
|
|
||||||
value: socketPath,
|
|
||||||
action: .copy(socketPath),
|
|
||||||
)
|
|
||||||
ConfigurationItemView(
|
|
||||||
title: .agentDetailsVersionTitle,
|
|
||||||
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
|
|
||||||
)
|
|
||||||
if let launchDate = process.launchDate {
|
|
||||||
ConfigurationItemView(
|
|
||||||
title: .agentDetailsRunningSinceTitle,
|
|
||||||
value: launchDate.formatted()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text(.agentRunningNoticeDetailTitle)
|
|
||||||
.font(.headline)
|
|
||||||
.padding(.top)
|
|
||||||
} footer: {
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
Text(.agentRunningNoticeDetailDescription)
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Menu(.agentDetailsRestartAgentButton) {
|
|
||||||
Button(.agentDetailsDisableAgentButton) {
|
|
||||||
Task {
|
|
||||||
_ = await LaunchAgentController()
|
|
||||||
.uninstall()
|
|
||||||
agentStatusChecker.check()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} primaryAction: {
|
|
||||||
Task {
|
|
||||||
let controller = LaunchAgentController()
|
|
||||||
let installed = await controller.install()
|
|
||||||
if !installed {
|
|
||||||
_ = await controller.forceLaunch()
|
|
||||||
}
|
|
||||||
agentStatusChecker.check()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
.frame(width: 400)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AgentNotRunningView: View {
|
|
||||||
|
|
||||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
|
||||||
@State var triedRestart = false
|
|
||||||
@State var loading = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
Section {
|
|
||||||
} header: {
|
|
||||||
Text(.agentNotRunningNoticeTitle)
|
|
||||||
.font(.headline)
|
|
||||||
.padding(.top)
|
|
||||||
} footer: {
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
Text(.agentNotRunningNoticeDetailDescription)
|
|
||||||
HStack {
|
|
||||||
if !triedRestart {
|
|
||||||
Spacer()
|
|
||||||
Button {
|
|
||||||
guard !loading else { return }
|
|
||||||
loading = true
|
|
||||||
Task {
|
|
||||||
let controller = LaunchAgentController()
|
|
||||||
let installed = await controller.install()
|
|
||||||
if !installed {
|
|
||||||
_ = await controller.forceLaunch()
|
|
||||||
}
|
|
||||||
agentStatusChecker.check()
|
|
||||||
loading = false
|
|
||||||
|
|
||||||
if !agentStatusChecker.running {
|
|
||||||
triedRestart = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
if !loading {
|
|
||||||
Text(.agentDetailsStartAgentButton)
|
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
Text(.agentDetailsStartAgentButtonStarting)
|
|
||||||
ProgressView()
|
|
||||||
.controlSize(.mini)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.primaryButton()
|
|
||||||
} else {
|
|
||||||
Text(.agentDetailsCouldNotStartError)
|
|
||||||
.bold()
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
.frame(width: 400)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
AgentStatusView()
|
|
||||||
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
|
|
||||||
}
|
|
||||||
#Preview {
|
|
||||||
AgentStatusView()
|
|
||||||
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ConfigurationItemView<Content: View>: View {
|
|
||||||
|
|
||||||
enum Action: Hashable {
|
|
||||||
case copy(String)
|
|
||||||
case revealInFinder(String)
|
|
||||||
}
|
|
||||||
|
|
||||||
let title: LocalizedStringResource
|
|
||||||
let content: Content
|
|
||||||
let action: Action?
|
|
||||||
|
|
||||||
init(title: LocalizedStringResource, value: String, action: Action? = nil) where Content == Text {
|
|
||||||
self.title = title
|
|
||||||
self.content = Text(value)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
self.action = action
|
|
||||||
}
|
|
||||||
|
|
||||||
init(title: LocalizedStringResource, action: Action? = nil, content: () -> Content) {
|
|
||||||
self.title = title
|
|
||||||
self.content = content()
|
|
||||||
self.action = action
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
HStack {
|
|
||||||
Text(title)
|
|
||||||
Spacer()
|
|
||||||
switch action {
|
|
||||||
case .copy(let string):
|
|
||||||
Button(.copyButton, systemImage: "document.on.document") {
|
|
||||||
NSPasteboard.general.declareTypes([.string], owner: nil)
|
|
||||||
NSPasteboard.general.setString(string, forType: .string)
|
|
||||||
}
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
case .revealInFinder(let rawPath):
|
|
||||||
Button(.revealInFinderButton, systemImage: "folder") {
|
|
||||||
// All foundation-based normalization methods replace this with the container directly.
|
|
||||||
let processedPath = rawPath.replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
|
|
||||||
let url = URL(filePath: processedPath)
|
|
||||||
let folder = url.deletingLastPathComponent().path()
|
|
||||||
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
|
|
||||||
}
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
case nil:
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ struct ContentView: View {
|
|||||||
toolbarItem(newItemView, id: "new")
|
toolbarItem(newItemView, id: "new")
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $runningSetup) {
|
.sheet(isPresented: $runningSetup) {
|
||||||
SetupView(setupComplete: $hasRunSetup)
|
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ extension ContentView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var needsSetup: Bool {
|
var needsSetup: Bool {
|
||||||
runningSetup || !hasRunSetup
|
(runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
|
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
|
||||||
@@ -66,7 +66,7 @@ extension ContentView {
|
|||||||
if needsSetup {
|
if needsSetup {
|
||||||
setupNoticeView
|
setupNoticeView
|
||||||
} else {
|
} else {
|
||||||
agentStatusToolbarView
|
runningNoticeView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ extension ContentView {
|
|||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.buttonStyle(ToolbarButtonStyle(color: color))
|
.buttonStyle(ToolbarButtonStyle(color: color))
|
||||||
.sheet(item: $selectedUpdate) { update in
|
.popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in
|
||||||
UpdateDetailView(update: update)
|
UpdateDetailView(update: update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,17 +103,18 @@ extension ContentView {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var newItemView: some View {
|
var newItemView: some View {
|
||||||
if storeList.modifiableStore?.isAvailable ?? false {
|
if storeList.modifiableStore?.isAvailable ?? false {
|
||||||
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
Button(action: {
|
||||||
showingCreation = true
|
showingCreation = true
|
||||||
}
|
}, label: {
|
||||||
.menuButton()
|
Image(systemName: "plus")
|
||||||
|
})
|
||||||
.sheet(isPresented: $showingCreation) {
|
.sheet(isPresented: $showingCreation) {
|
||||||
if let modifiable = storeList.modifiableStore {
|
if let modifiable = storeList.modifiableStore {
|
||||||
CreateSecretView(store: modifiable) { created in
|
CreateSecretView(store: modifiable, showing: $showingCreation)
|
||||||
if let created {
|
.onDisappear {
|
||||||
activeSecret = created
|
guard let newest = modifiable.secrets.last else { return }
|
||||||
|
activeSecret = newest
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,44 +125,43 @@ extension ContentView {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
runningSetup = true
|
runningSetup = true
|
||||||
}, label: {
|
}, label: {
|
||||||
if !hasRunSetup {
|
Group {
|
||||||
Text(.agentSetupNoticeTitle)
|
if hasRunSetup && !agentStatusChecker.running {
|
||||||
.font(.headline)
|
Text(.agentNotRunningNoticeTitle)
|
||||||
|
} else {
|
||||||
|
Text(.agentSetupNoticeTitle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
})
|
})
|
||||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var agentStatusToolbarView: some View {
|
var runningNoticeView: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showingAgentInfo = true
|
showingAgentInfo = true
|
||||||
}, label: {
|
}, label: {
|
||||||
HStack {
|
HStack {
|
||||||
if agentStatusChecker.running {
|
Text(.agentRunningNoticeTitle)
|
||||||
Text(.agentRunningNoticeTitle)
|
.font(.headline)
|
||||||
.font(.headline)
|
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||||
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
Circle()
|
||||||
Circle()
|
.frame(width: 10, height: 10)
|
||||||
.frame(width: 10, height: 10)
|
.foregroundColor(Color.green)
|
||||||
.foregroundColor(Color.green)
|
|
||||||
} else {
|
|
||||||
Text(.agentNotRunningNoticeTitle)
|
|
||||||
.font(.headline)
|
|
||||||
Circle()
|
|
||||||
.frame(width: 10, height: 10)
|
|
||||||
.foregroundColor(Color.red)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.buttonStyle(
|
.buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05)))
|
||||||
ToolbarButtonStyle(
|
|
||||||
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
|
||||||
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||||
AgentStatusView()
|
VStack {
|
||||||
|
Text(.agentRunningNoticeDetailTitle)
|
||||||
|
.font(.title)
|
||||||
|
.padding(5)
|
||||||
|
Text(.agentRunningNoticeDetailDescription)
|
||||||
|
.frame(width: 300)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +193,7 @@ extension ContentView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var attachmentAnchor: PopoverAttachmentAnchor {
|
var attachmentAnchor: PopoverAttachmentAnchor {
|
||||||
|
// Ideally .point(.bottom), but broken on Sonoma (FB12726503)
|
||||||
.rect(.bounds)
|
.rect(.bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ import SecretKit
|
|||||||
struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||||
|
|
||||||
@State var store: StoreType
|
@State var store: StoreType
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Binding var showing: Bool
|
||||||
var createdSecret: (AnySecret?) -> Void
|
|
||||||
|
|
||||||
@State private var name = ""
|
@State private var name = ""
|
||||||
@State private var keyAttribution = ""
|
@State private var keyAttribution = ""
|
||||||
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
|
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
|
||||||
@State private var keyType: KeyType?
|
@State private var keyType: KeyType?
|
||||||
@State var advanced = false
|
@State var advanced = false
|
||||||
@State var errorText: String?
|
|
||||||
|
|
||||||
private var authenticationOptions: [AuthenticationRequirement] {
|
private var authenticationOptions: [AuthenticationRequirement] {
|
||||||
if advanced || authenticationRequirement == .biometryCurrent {
|
if advanced || authenticationRequirement == .biometryCurrent {
|
||||||
@@ -96,24 +94,16 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let errorText {
|
|
||||||
Section {
|
|
||||||
} footer: {
|
|
||||||
Text(verbatim: errorText)
|
|
||||||
.errorStyle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Toggle(.createSecretAdvancedLabel, isOn: $advanced)
|
Toggle(.createSecretAdvancedLabel, isOn: $advanced)
|
||||||
.toggleStyle(.button)
|
.toggleStyle(.button)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(.createSecretCancelButton, role: .cancel) {
|
Button(.createSecretCancelButton, role: .cancel) {
|
||||||
dismiss()
|
showing = false
|
||||||
}
|
}
|
||||||
Button(.createSecretCreateButton, action: save)
|
Button(.createSecretCreateButton, action: save)
|
||||||
.keyboardShortcut(.return)
|
.primary()
|
||||||
.primaryButton()
|
|
||||||
.disabled(name.isEmpty)
|
.disabled(name.isEmpty)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@@ -127,25 +117,20 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
func save() {
|
func save() {
|
||||||
let attribution = keyAttribution.isEmpty ? nil : keyAttribution
|
let attribution = keyAttribution.isEmpty ? nil : keyAttribution
|
||||||
Task {
|
Task {
|
||||||
do {
|
try! await store.create(
|
||||||
let new = try await store.create(
|
name: name,
|
||||||
name: name,
|
attributes: .init(
|
||||||
attributes: .init(
|
keyType: keyType!,
|
||||||
keyType: keyType!,
|
authentication: authenticationRequirement,
|
||||||
authentication: authenticationRequirement,
|
publicKeyAttribution: attribution
|
||||||
publicKeyAttribution: attribution
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
createdSecret(AnySecret(new))
|
)
|
||||||
dismiss()
|
showing = false
|
||||||
} catch {
|
|
||||||
errorText = error.localizedDescription
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
CreateSecretView(store: Preview.StoreModifiable()) { _ in }
|
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ struct DeleteSecretConfirmationModifier: ViewModifier {
|
|||||||
TextField(secret.name, text: $confirmedSecretName)
|
TextField(secret.name, text: $confirmedSecretName)
|
||||||
if let errorText {
|
if let errorText {
|
||||||
Text(verbatim: errorText)
|
Text(verbatim: errorText)
|
||||||
.errorStyle()
|
.foregroundStyle(.red)
|
||||||
|
.font(.callout)
|
||||||
}
|
}
|
||||||
Button(.deleteConfirmationDeleteButton, action: delete)
|
Button(.deleteConfirmationDeleteButton, action: delete)
|
||||||
.disabled(confirmedSecretName != secret.name)
|
.disabled(confirmedSecretName != secret.name)
|
||||||
|
|||||||
@@ -30,22 +30,21 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
} footer: {
|
}
|
||||||
if let errorText {
|
if let errorText {
|
||||||
Text(verbatim: errorText)
|
Text(verbatim: errorText)
|
||||||
.errorStyle()
|
.foregroundStyle(.red)
|
||||||
}
|
.font(.callout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
|
Button(.editSaveButton, action: rename)
|
||||||
|
.disabled(name.isEmpty)
|
||||||
|
.keyboardShortcut(.return)
|
||||||
Button(.editCancelButton) {
|
Button(.editCancelButton) {
|
||||||
dismissalBlock(false)
|
dismissalBlock(false)
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.cancelAction)
|
.keyboardShortcut(.cancelAction)
|
||||||
Button(.editSaveButton, action: rename)
|
|
||||||
.disabled(name.isEmpty)
|
|
||||||
.keyboardShortcut(.return)
|
|
||||||
.primaryButton()
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
@@ -54,9 +53,7 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
|
|
||||||
func rename() {
|
func rename() {
|
||||||
var attributes = secret.attributes
|
var attributes = secret.attributes
|
||||||
if !publicKeyAttribution.isEmpty {
|
attributes.publicKeyAttribution = publicKeyAttribution.isEmpty ? nil : publicKeyAttribution
|
||||||
attributes.publicKeyAttribution = publicKeyAttribution
|
|
||||||
}
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await store.update(secret: secret, name: name, attributes: attributes)
|
try await store.update(secret: secret, name: name, attributes: attributes)
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ErrorStyleModifier: ViewModifier {
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.font(.callout)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
func errorStyle() -> some View {
|
|
||||||
modifier(ErrorStyleModifier())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct IntegrationsView: View {
|
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
@State private var selectedInstruction: ConfigurationFileInstructions?
|
|
||||||
private let instructions = Instructions()
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationSplitView {
|
|
||||||
List(selection: $selectedInstruction) {
|
|
||||||
ForEach(instructions.instructions) { group in
|
|
||||||
Section(group.name) {
|
|
||||||
ForEach(group.instructions) { instruction in
|
|
||||||
Text(instruction.tool)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.tag(instruction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} detail: {
|
|
||||||
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
|
||||||
.fauxToolbar {
|
|
||||||
Button(.setupDoneButton) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.normalButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
selectedInstruction = instructions.gettingStarted
|
|
||||||
}
|
|
||||||
.frame(minHeight: 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
|
|
||||||
modifier(FauxToolbarModifier(toolbarContent: content()))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
|
|
||||||
|
|
||||||
var toolbarContent: ToolbarContent
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
content
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
toolbarContent
|
|
||||||
.padding(.top, 8)
|
|
||||||
.padding(.trailing, 16)
|
|
||||||
.padding(.bottom, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct IntegrationsDetailView: View {
|
|
||||||
|
|
||||||
@Binding private var selectedInstruction: ConfigurationFileInstructions?
|
|
||||||
private let instructions = Instructions()
|
|
||||||
|
|
||||||
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
|
|
||||||
_selectedInstruction = selectedInstruction
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
if let selectedInstruction {
|
|
||||||
switch selectedInstruction.id {
|
|
||||||
case .gettingStarted:
|
|
||||||
Form {
|
|
||||||
Section(.integrationsGettingStartedTitle) {
|
|
||||||
Text(.integrationsGettingStartedTitleDescription)
|
|
||||||
}
|
|
||||||
Section {
|
|
||||||
Group {
|
|
||||||
Text(.integrationsGettingStartedSuggestionSsh)
|
|
||||||
.onTapGesture {
|
|
||||||
self.selectedInstruction = instructions.ssh
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text(.integrationsGettingStartedSuggestionShell)
|
|
||||||
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool))
|
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
.onTapGesture {
|
|
||||||
self.selectedInstruction = instructions.defaultShell
|
|
||||||
}
|
|
||||||
Text(.integrationsGettingStartedSuggestionGit)
|
|
||||||
.onTapGesture {
|
|
||||||
self.selectedInstruction = instructions.git
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foregroundStyle(.link)
|
|
||||||
|
|
||||||
} header: {
|
|
||||||
Text(.integrationsGettingStartedWhatShouldIConfigureTitle)
|
|
||||||
}
|
|
||||||
footer: {
|
|
||||||
Text(.integrationsGettingStartedMultipleConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
case .tool:
|
|
||||||
Form {
|
|
||||||
ForEach(selectedInstruction.steps) { stepGroup in
|
|
||||||
Section {
|
|
||||||
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
|
|
||||||
ForEach(stepGroup.steps, id: \.self) { step in
|
|
||||||
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(step)) {
|
|
||||||
HStack {
|
|
||||||
Text(step)
|
|
||||||
.padding(8)
|
|
||||||
.font(.system(.subheadline, design: .monospaced))
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background {
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.fill(.black.opacity(0.05))
|
|
||||||
.stroke(.separator, lineWidth: 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} footer: {
|
|
||||||
if let note = stepGroup.note {
|
|
||||||
Text(note)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let url = selectedInstruction.website {
|
|
||||||
Section {
|
|
||||||
Link(destination: url) {
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text(.integrationsWebLink)
|
|
||||||
.font(.headline)
|
|
||||||
Text(url.absoluteString)
|
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
case .otherShell:
|
|
||||||
Form {
|
|
||||||
Section {
|
|
||||||
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!)
|
|
||||||
} header: {
|
|
||||||
Text(.integrationsCommunityShellListDescription)
|
|
||||||
.font(.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
|
|
||||||
case .otherApp:
|
|
||||||
Form {
|
|
||||||
Section {
|
|
||||||
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!)
|
|
||||||
} header: {
|
|
||||||
Text(.integrationsCommunityAppsListDescription)
|
|
||||||
.font(.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct Instructions {
|
|
||||||
|
|
||||||
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
|
||||||
|
|
||||||
|
|
||||||
var defaultShell: ConfigurationFileInstructions {
|
|
||||||
zsh
|
|
||||||
}
|
|
||||||
|
|
||||||
var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted)
|
|
||||||
|
|
||||||
var ssh: ConfigurationFileInstructions {
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "SSH",
|
|
||||||
configPath: "~/.ssh/config",
|
|
||||||
configText: "Host *\n\tIdentityAgent \(socketPath)",
|
|
||||||
website: URL(string: "https://man.openbsd.org/ssh_config.5")!,
|
|
||||||
note: "You can tell SSH to use a specific key for a given host. See the web documentation for more details.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var git: ConfigurationFileInstructions {
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "Git Signing",
|
|
||||||
steps: [
|
|
||||||
.init(path: "~/.gitconfig", steps: [
|
|
||||||
"""
|
|
||||||
[user]
|
|
||||||
signingkey = YOUR_PUBLIC_KEY_PATH
|
|
||||||
[commit]
|
|
||||||
gpgsign = true
|
|
||||||
[gpg]
|
|
||||||
format = ssh
|
|
||||||
[gpg "ssh"]
|
|
||||||
allowedSignersFile = ~/.gitallowedsigners
|
|
||||||
"""
|
|
||||||
],
|
|
||||||
note: "If any section (like [user]) already exists, just add the entries in the existing section."
|
|
||||||
|
|
||||||
),
|
|
||||||
.init(
|
|
||||||
path: "~/.gitallowedsigners",
|
|
||||||
steps: [
|
|
||||||
"YOUR_PUBLIC_KEY"
|
|
||||||
],
|
|
||||||
note: "~/.gitallowedsigners probably does not exist. You'll need to create it."
|
|
||||||
),
|
|
||||||
],
|
|
||||||
website: URL(string: "https://git-scm.com/docs/git-config")!,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var zsh: ConfigurationFileInstructions {
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "zsh",
|
|
||||||
configPath: "~/.zshrc",
|
|
||||||
configText: "export SSH_AUTH_SOCK=\(socketPath)"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var instructions: [ConfigurationGroup] {
|
|
||||||
[
|
|
||||||
ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [
|
|
||||||
gettingStarted
|
|
||||||
]),
|
|
||||||
ConfigurationGroup(
|
|
||||||
name: .integrationsSystemSectionTitle,
|
|
||||||
instructions: [
|
|
||||||
ssh,
|
|
||||||
git,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [
|
|
||||||
zsh,
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "bash",
|
|
||||||
configPath: "~/.bashrc",
|
|
||||||
configText: "export SSH_AUTH_SOCK=\(socketPath)"
|
|
||||||
),
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "fish",
|
|
||||||
configPath: "~/.config/fish/config.fish",
|
|
||||||
configText: "set -x SSH_AUTH_SOCK \(socketPath)"
|
|
||||||
),
|
|
||||||
ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell),
|
|
||||||
]),
|
|
||||||
ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [
|
|
||||||
ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConfigurationGroup: Identifiable {
|
|
||||||
let id = UUID()
|
|
||||||
var name: LocalizedStringResource
|
|
||||||
var instructions: [ConfigurationFileInstructions] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConfigurationFileInstructions: Hashable, Identifiable {
|
|
||||||
|
|
||||||
struct StepGroup: Hashable, Identifiable {
|
|
||||||
let path: String
|
|
||||||
let steps: [String]
|
|
||||||
let note: String?
|
|
||||||
var id: String { path }
|
|
||||||
|
|
||||||
init(path: String, steps: [String], note: String? = nil) {
|
|
||||||
self.path = path
|
|
||||||
self.steps = steps
|
|
||||||
self.note = note
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var id: ID
|
|
||||||
var tool: String
|
|
||||||
var steps: [StepGroup]
|
|
||||||
var website: URL?
|
|
||||||
|
|
||||||
init(tool: String, configPath: String, configText: String, website: URL? = nil, note: String? = nil) {
|
|
||||||
self.id = .tool(tool)
|
|
||||||
self.tool = tool
|
|
||||||
self.steps = [StepGroup(path: configPath, steps: [configText], note: note)]
|
|
||||||
self.website = website
|
|
||||||
}
|
|
||||||
|
|
||||||
init(tool: String, steps: [StepGroup], website: URL? = nil) {
|
|
||||||
self.id = .tool(tool)
|
|
||||||
self.tool = tool
|
|
||||||
self.steps = steps
|
|
||||||
self.website = website
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ name: LocalizedStringResource, id: ID) {
|
|
||||||
self.id = id
|
|
||||||
tool = String(localized: name)
|
|
||||||
self.steps = []
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ID: Identifiable, Hashable {
|
|
||||||
case gettingStarted
|
|
||||||
case tool(String)
|
|
||||||
case otherShell
|
|
||||||
case otherApp
|
|
||||||
|
|
||||||
var id: String {
|
|
||||||
switch self {
|
|
||||||
case .gettingStarted:
|
|
||||||
"getting_started"
|
|
||||||
case .tool(let name):
|
|
||||||
name
|
|
||||||
case .otherShell:
|
|
||||||
"other_shell"
|
|
||||||
case .otherApp:
|
|
||||||
"other_app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
IntegrationsView()
|
|
||||||
.frame(height: 500)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ struct SecretDetailView<SecretType: Secret>: View {
|
|||||||
let secret: SecretType
|
let secret: SecretType
|
||||||
|
|
||||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID))
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -37,14 +37,12 @@ struct SecretDetailView<SecretType: Secret>: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension URL {
|
#if DEBUG
|
||||||
|
|
||||||
static var agentHomeURL: URL {
|
struct SecretDetailView_Previews: PreviewProvider {
|
||||||
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
|
static var previews: some View {
|
||||||
|
SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#endif
|
||||||
SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,172 +2,227 @@ import SwiftUI
|
|||||||
|
|
||||||
struct SetupView: View {
|
struct SetupView: View {
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@State var stepIndex = 0
|
||||||
|
@Binding var visible: Bool
|
||||||
@Binding var setupComplete: Bool
|
@Binding var setupComplete: Bool
|
||||||
|
|
||||||
@State var showingIntegrations = false
|
var body: some View {
|
||||||
@State var buttonWidth: CGFloat?
|
GeometryReader { proxy in
|
||||||
|
VStack {
|
||||||
|
StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width)
|
||||||
|
GeometryReader { _ in
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
SecretAgentSetupView(buttonAction: advance)
|
||||||
|
.frame(width: proxy.size.width)
|
||||||
|
SSHAgentSetupView(buttonAction: advance)
|
||||||
|
.frame(width: proxy.size.width)
|
||||||
|
UpdaterExplainerView {
|
||||||
|
visible = false
|
||||||
|
setupComplete = true
|
||||||
|
}
|
||||||
|
.frame(width: proxy.size.width)
|
||||||
|
}
|
||||||
|
.offset(x: -proxy.size.width * Double(stepIndex), y: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500)
|
||||||
|
}
|
||||||
|
|
||||||
@State var installed = false
|
|
||||||
@State var updates = false
|
func advance() {
|
||||||
@State var integrations = false
|
withAnimation(.spring()) {
|
||||||
var allDone: Bool {
|
stepIndex += 1
|
||||||
installed && updates && integrations
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StepView: View {
|
||||||
|
|
||||||
|
let numberOfSteps: Int
|
||||||
|
let currentStep: Int
|
||||||
|
|
||||||
|
// Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7
|
||||||
|
let width: Double
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.frame(height: 5)
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5)
|
||||||
|
HStack {
|
||||||
|
ForEach(Array(0..<numberOfSteps), id: \.self) { index in
|
||||||
|
ZStack {
|
||||||
|
if currentStep > index {
|
||||||
|
Circle()
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
|
||||||
|
Text(.setupStepCompleteSymbol)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.bold()
|
||||||
|
} else {
|
||||||
|
Circle()
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
|
||||||
|
if currentStep == index {
|
||||||
|
Circle()
|
||||||
|
.strokeBorder(Color.white, lineWidth: 3)
|
||||||
|
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
|
||||||
|
}
|
||||||
|
Text(String(describing: index + 1))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index < numberOfSteps - 1 {
|
||||||
|
Spacer(minLength: 30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.padding(Constants.padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StepView {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
|
||||||
|
static let padding: Double = 15
|
||||||
|
static let circleWidth: Double = 30
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SetupStepView<Content> : View where Content : View {
|
||||||
|
|
||||||
|
let title: LocalizedStringResource
|
||||||
|
let image: Image
|
||||||
|
let bodyText: LocalizedStringResource
|
||||||
|
let buttonTitle: LocalizedStringResource
|
||||||
|
let buttonAction: () -> Void
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(title: LocalizedStringResource, image: Image, bodyText: LocalizedStringResource, buttonTitle: LocalizedStringResource, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
|
||||||
|
self.title = title
|
||||||
|
self.image = image
|
||||||
|
self.bodyText = bodyText
|
||||||
|
self.buttonTitle = buttonTitle
|
||||||
|
self.buttonAction = buttonAction
|
||||||
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
Text(title)
|
||||||
StepView(
|
.font(.title)
|
||||||
title: .setupAgentTitle,
|
Spacer()
|
||||||
description: .setupAgentDescription,
|
image
|
||||||
systemImage: "lock.laptopcomputer",
|
|
||||||
) {
|
|
||||||
setupButton(
|
|
||||||
.setupAgentInstallButton,
|
|
||||||
complete: installed,
|
|
||||||
width: buttonWidth
|
|
||||||
) {
|
|
||||||
installed = true
|
|
||||||
Task {
|
|
||||||
await LaunchAgentController().install()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
StepView(
|
|
||||||
title: .setupUpdatesTitle,
|
|
||||||
description: .setupUpdatesDescription,
|
|
||||||
systemImage: "network.badge.shield.half.filled",
|
|
||||||
) {
|
|
||||||
setupButton(
|
|
||||||
.setupUpdatesOkButton,
|
|
||||||
complete: updates,
|
|
||||||
width: buttonWidth
|
|
||||||
) {
|
|
||||||
updates = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
StepView(
|
|
||||||
title: .setupIntegrationsTitle,
|
|
||||||
description: .setupIntegrationsDescription,
|
|
||||||
systemImage: "firewall",
|
|
||||||
) {
|
|
||||||
setupButton(
|
|
||||||
.setupIntegrationsButton,
|
|
||||||
complete: integrations,
|
|
||||||
width: buttonWidth
|
|
||||||
) {
|
|
||||||
showingIntegrations = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onPreferenceChange(setupButton.WidthKey.self) { width in
|
|
||||||
buttonWidth = width
|
|
||||||
}
|
|
||||||
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
|
||||||
.frame(minWidth: 700, maxWidth: .infinity)
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Button(.setupDoneButton) {
|
|
||||||
setupComplete = true
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.disabled(!allDone)
|
|
||||||
.primaryButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.interactiveDismissDisabled()
|
|
||||||
.padding()
|
|
||||||
.sheet(isPresented: $showingIntegrations, onDismiss: {
|
|
||||||
integrations = true
|
|
||||||
}, content: {
|
|
||||||
IntegrationsView()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct setupButton: View {
|
|
||||||
|
|
||||||
struct WidthKey: @MainActor PreferenceKey {
|
|
||||||
@MainActor static var defaultValue: CGFloat? = nil
|
|
||||||
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
|
|
||||||
if let next = nextValue(), next > (value ?? -1) {
|
|
||||||
value = next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
let label: LocalizedStringResource
|
|
||||||
let complete: Bool
|
|
||||||
let action: () -> Void
|
|
||||||
let width: CGFloat?
|
|
||||||
@State var currentWidth: CGFloat?
|
|
||||||
|
|
||||||
init(_ label: LocalizedStringResource, complete: Bool, width: CGFloat? = nil, action: @escaping () -> Void) {
|
|
||||||
self.label = label
|
|
||||||
self.complete = complete
|
|
||||||
self.action = action
|
|
||||||
self.width = width
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
if complete {
|
|
||||||
Text(.setupStepCompleteButton)
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
} else {
|
|
||||||
Text(label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: width)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.onGeometryChange(for: CGFloat.self) { proxy in
|
|
||||||
proxy.size.width
|
|
||||||
} action: { newValue in
|
|
||||||
currentWidth = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preference(key: WidthKey.self, value: currentWidth)
|
|
||||||
.primaryButton()
|
|
||||||
.disabled(complete)
|
|
||||||
.tint(complete ? .green : nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StepView<Content: View>: View {
|
|
||||||
|
|
||||||
let title: LocalizedStringResource
|
|
||||||
let icon: Image
|
|
||||||
let description: LocalizedStringResource
|
|
||||||
let actions: Content
|
|
||||||
|
|
||||||
init(title: LocalizedStringResource, description: LocalizedStringResource, systemImage: String, actions: () -> Content) {
|
|
||||||
self.title = title
|
|
||||||
self.icon = Image(systemName: systemImage)
|
|
||||||
self.description = description
|
|
||||||
self.actions = actions()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
icon
|
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 24)
|
.frame(width: 64)
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
Text(title)
|
|
||||||
.bold()
|
|
||||||
Text(description)
|
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
actions
|
Text(bodyText)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
Spacer()
|
||||||
|
content
|
||||||
|
Spacer()
|
||||||
|
Button(buttonTitle) {
|
||||||
|
buttonAction()
|
||||||
|
}
|
||||||
|
}.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SecretAgentSetupView: View {
|
||||||
|
|
||||||
|
let buttonAction: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
SetupStepView(title: .setupAgentTitle,
|
||||||
|
image: Image(nsImage: NSApplication.shared.applicationIconImage),
|
||||||
|
bodyText: .setupAgentDescription,
|
||||||
|
buttonTitle: .setupAgentInstallButton,
|
||||||
|
buttonAction: install) {
|
||||||
|
Text(.setupAgentActivityMonitorDescription)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func install() {
|
||||||
|
Task {
|
||||||
|
await LaunchAgentController().install()
|
||||||
|
buttonAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SSHAgentSetupView: View {
|
||||||
|
|
||||||
|
let buttonAction: () -> Void
|
||||||
|
|
||||||
|
private static let controller = ShellConfigurationController()
|
||||||
|
@State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first!
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
SetupStepView(title: .setupSshTitle,
|
||||||
|
image: Image(systemName: "terminal"),
|
||||||
|
bodyText: .setupSshDescription,
|
||||||
|
buttonTitle: .setupSshAddedManuallyButton,
|
||||||
|
buttonAction: buttonAction) {
|
||||||
|
Link(.setupThirdPartyFaqLink, destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!)
|
||||||
|
Picker(selection: $selectedShellInstruction, label: EmptyView()) {
|
||||||
|
ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in
|
||||||
|
Text(instruction.shell)
|
||||||
|
.tag(instruction)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}.pickerStyle(SegmentedPickerStyle())
|
||||||
|
CopyableView(title: .setupSshAddToConfigButton(configPath: selectedShellInstruction.shellConfigPath), image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
|
||||||
|
Button(.setupSshAddForMeButton) {
|
||||||
|
let controller = ShellConfigurationController()
|
||||||
|
if controller.addToShell(shellInstructions: selectedShellInstruction) {
|
||||||
|
buttonAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Delegate: NSObject, NSOpenSavePanelDelegate {
|
||||||
|
|
||||||
|
private let name: String
|
||||||
|
|
||||||
|
init(name: String) {
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
|
||||||
|
return url.lastPathComponent == name
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UpdaterExplainerView: View {
|
||||||
|
|
||||||
|
let buttonAction: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
SetupStepView(title: .setupUpdatesTitle,
|
||||||
|
image: Image(systemName: "dot.radiowaves.left.and.right"),
|
||||||
|
bodyText: .setupUpdatesDescription,
|
||||||
|
buttonTitle: .setupUpdatesOk,
|
||||||
|
buttonAction: buttonAction) {
|
||||||
|
Link(.setupUpdatesReadmore, destination: SetupView.Constants.updaterFAQURL)
|
||||||
}
|
}
|
||||||
.padding(20)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -180,6 +235,63 @@ extension SetupView {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
struct ShellConfigInstruction: Identifiable, Hashable {
|
||||||
SetupView(setupComplete: .constant(false))
|
|
||||||
|
var shell: String
|
||||||
|
var shellConfigDirectory: String
|
||||||
|
var shellConfigFilename: String
|
||||||
|
var text: String
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
shell
|
||||||
|
}
|
||||||
|
|
||||||
|
var shellConfigPath: String {
|
||||||
|
return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
struct SetupView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
SetupView(visible: .constant(true), setupComplete: .constant(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SecretAgentSetupView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
SecretAgentSetupView(buttonAction: {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SSHAgentSetupView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
SSHAgentSetupView(buttonAction: {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UpdaterExplainerView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UpdaterExplainerView(buttonAction: {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ struct UpdateDetailView: View {
|
|||||||
Text(.updateVersionName(updateName: update.name)).font(.title)
|
Text(.updateVersionName(updateName: update.name)).font(.title)
|
||||||
GroupBox(label: Text(.updateReleaseNotesTitle)) {
|
GroupBox(label: Text(.updateReleaseNotesTitle)) {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Text(attributedBody)
|
attributedBody
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
@@ -35,62 +35,29 @@ struct UpdateDetailView: View {
|
|||||||
.frame(maxWidth: 500)
|
.frame(maxWidth: 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
var attributedBody: AttributedString {
|
var attributedBody: Text {
|
||||||
do {
|
var text = Text(verbatim: "")
|
||||||
var text = try AttributedString(
|
for line in update.body.split(whereSeparator: \.isNewline) {
|
||||||
markdown: update.body,
|
let attributed: Text
|
||||||
options: .init(
|
let split = line.split(separator: " ")
|
||||||
allowsExtendedAttributes: true,
|
let unprefixed = split.dropFirst().joined(separator: " ")
|
||||||
interpretedSyntax: .full,
|
if let prefix = split.first {
|
||||||
),
|
switch prefix {
|
||||||
baseURL: URL(string: "https://github.com/maxgoedjen/secretive")!
|
case "#":
|
||||||
)
|
attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n")
|
||||||
.transformingAttributes(AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self) { key in
|
case "##":
|
||||||
let font: Font? = switch key.value?.components.first?.kind {
|
attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n")
|
||||||
case .header(level: 1):
|
case "###":
|
||||||
Font.title
|
attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n")
|
||||||
case .header(level: 2):
|
|
||||||
Font.title2
|
|
||||||
case .header(level: 3):
|
|
||||||
Font.title3
|
|
||||||
default:
|
default:
|
||||||
nil
|
attributed = Text(line) + Text(verbatim: "\n\n")
|
||||||
}
|
|
||||||
if let font {
|
|
||||||
key.replace(with: AttributeScopes.SwiftUIAttributes.FontAttribute.self, value: font)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
attributed = Text(line) + Text(verbatim: "\n\n")
|
||||||
}
|
}
|
||||||
let lineBreak = AttributedString("\n\n")
|
text = text + attributed
|
||||||
for run in text.runs.reversed() {
|
|
||||||
text.insert(lineBreak, at: run.range.lowerBound)
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
} catch {
|
|
||||||
var text = AttributedString()
|
|
||||||
for line in update.body.split(whereSeparator: \.isNewline) {
|
|
||||||
let attributed: AttributedString
|
|
||||||
let split = line.split(separator: " ")
|
|
||||||
let unprefixed = split.dropFirst().joined(separator: " ")
|
|
||||||
if let prefix = split.first {
|
|
||||||
var container = AttributeContainer()
|
|
||||||
switch prefix {
|
|
||||||
case "#":
|
|
||||||
container.font = .title
|
|
||||||
case "##":
|
|
||||||
container.font = .title2
|
|
||||||
case "###":
|
|
||||||
container.font = .title3
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attributed = AttributedString(unprefixed, attributes: container)
|
|
||||||
} else {
|
|
||||||
attributed = AttributedString(line + "\n\n")
|
|
||||||
}
|
|
||||||
text = text + attributed
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user