diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d5fb7f3 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,47 @@ +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '26 15 * * 3' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + # Disable this until CodeQL supports Xcode 26 builds. + # - language: swift + # build-mode: manual + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - if: matrix.build-mode == 'manual' + name: "Select Xcode" + run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + - if: matrix.build-mode == 'manual' + name: "Build" + run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 53735cf..2ffe90a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -3,10 +3,15 @@ name: Nightly on: schedule: - cron: "0 8 * * *" + workflow_dispatch: + jobs: build: -# runs-on: macOS-latest runs-on: macos-15 + permissions: + id-token: write + contents: write + attestations: write timeout-minutes: 10 steps: - uses: actions/checkout@v5 @@ -25,27 +30,29 @@ jobs: env: RUN_ID: ${{ github.run_id }} run: | - sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0/g" Sources/Config/Config.xcconfig + DATE=$(date "+%Y-%m-%d") + sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0_nightly-$DATE/g" Sources/Config/Config.xcconfig sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf - name: Build run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive - - name: Create ZIPs + - name: Create ZIP run: | ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip - ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip - name: Notarize env: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip - - name: Attest - id: attest - uses: actions/attest-build-provenance@v2 - with: - subject-path: 'Secretive.zip' - name: Upload App to Artifacts + id: upload uses: actions/upload-artifact@v4 with: name: Secretive.zip path: Secretive.zip + - name: Attest + id: attest + uses: actions/attest-build-provenance@v2 + with: + subject-name: "Secretive.zip" + subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ebd0b01..9d69dfa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,8 @@ on: - '*' jobs: test: -# runs-on: macOS-latest + permissions: + contents: read runs-on: macos-15 timeout-minutes: 10 steps: @@ -25,12 +26,11 @@ jobs: - name: Test run: swift test --build-system swiftbuild --package-path Sources/Packages build: -# runs-on: macOS-latest - runs-on: macos-15 permissions: id-token: write contents: write attestations: write + runs-on: macos-15 timeout-minutes: 10 steps: - uses: actions/checkout@v5 @@ -56,39 +56,34 @@ jobs: sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf - name: Build run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive - - name: Create ZIPs + - name: Create ZIP run: | ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip - ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Xcode_Archive.zip - name: Notarize env: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip + - name: Upload App to Artifacts + id: upload + uses: actions/upload-artifact@v4 + with: + name: Secretive.zip + path: Secretive.zip - name: Attest id: attest uses: actions/attest-build-provenance@v2 with: - subject-path: 'Secretive.zip, Xcode_Archive.zip' + subject-name: "Secretive.zip" + subject-digest: ${{ steps.upload.outputs.artifact-digest }} - name: Create Release run: | sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md gh release create $TAG_NAME -d -F .github/templates/release.md gh release upload Secretive.zip - gh release upload Xcode_Archive.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_NAME: ${{ github.ref }} RUN_ID: ${{ github.run_id }} ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }} - - name: Upload App to Artifacts - uses: actions/upload-artifact@v4 - with: - name: Secretive.zip - path: Secretive.zip - - name: Upload Archive to Artifacts - uses: actions/upload-artifact@v4 - with: - name: Xcode_Archive.zip - path: Xcode_Archive.zip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e790e2f..2fb5150 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,8 @@ name: Test on: [push, pull_request] jobs: test: -# runs-on: macOS-latest + permissions: + contents: read runs-on: macos-15 timeout-minutes: 10 steps: diff --git a/Package.swift b/Package.swift index 700486e..2ba06ef 100644 --- a/Package.swift +++ b/Package.swift @@ -57,7 +57,7 @@ let package = Package( ) var localization: Resource { - .process("../../Localizable.xcstrings") + .process("../../Resources/Localizable.xcstrings") } var swiftSettings: [PackageDescription.SwiftSetting] { diff --git a/README.md b/README.md index 50d8cd1..66d2b04 100644 --- a/README.md +++ b/README.md @@ -61,4 +61,4 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b ## Security -If you discover any vulnerabilities in this project, please notify [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with the subject containing "SECRETIVE SECURITY." +Secretive's security policy is detailed in [SECURITY.md](SECURITY.md). To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) diff --git a/SECURITY.md b/SECURITY.md index 94d1da3..63412c6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -24,4 +24,4 @@ The latest version on the [Releases page](https://github.com/maxgoedjen/secretiv ## Reporting a Vulnerability -If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com with the subject containing "SECRETIVE SECURITY." +To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 82322b2..8acfb24 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -82,7 +82,7 @@ let package = Package( ) var localization: Resource { - .process("../../Localizable.xcstrings") + .process("../../Resources/Localizable.xcstrings") } var swiftSettings: [PackageDescription.SwiftSetting] { diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Resources/Localizable.xcstrings similarity index 92% rename from Sources/Packages/Localizable.xcstrings rename to Sources/Packages/Resources/Localizable.xcstrings index 6e75bf7..89f8b34 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Resources/Localizable.xcstrings @@ -1,6 +1,108 @@ { "sourceLanguage" : "en", "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" : { "extractionState" : "manual", "localizations" : { @@ -386,6 +488,17 @@ } } }, + "agentDetailsLocationTitle" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Agent Location" + } + } + } + }, "app_menu_help_button" : { "extractionState" : "manual", "localizations" : { @@ -1161,6 +1274,17 @@ } } }, + "copy_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" + } + } + } + }, "copyable_click_to_copy_button" : { "extractionState" : "manual", "localizations" : { @@ -1497,7 +1621,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "This shows at the end of your public key." + "value" : "This shows at the end of your public key. It’s usually an email address." } } } @@ -2777,6 +2901,28 @@ } } }, + "empty_store_modifiable_empty_os_warning_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "It looks like you may have recently updated macOS. Sometimes this puts the Secure Enclave into a weird state, and you might need to reboot your Mac before things start working again." + } + } + } + }, + "empty_store_modifiable_empty_os_warning_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Missing Secrets?" + } + } + } + }, "empty_store_nonmodifiable_description" : { "extractionState" : "manual", "localizations" : { @@ -2859,73 +3005,73 @@ "localizations" : { "ca" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048." } }, "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive unterstützt EC256, EC384, RSA1024 und RSA2048 Schlüssel." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Secretive supports EC256, EC384, RSA1024, and RSA2048 keys." + "value" : "Secretive supports EC256, EC384, and RSA2048 keys." } }, "fi" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia." } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive prend en charge les clés EC256, EC384, RSA1024 et RSA2048." } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive supporta la cifratura EC256, EC384, RSA1024 e RSA2048." } }, "ja" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。" } }, "ko" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다." } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive wspiera klucze EC256, EC384, RSA1024 i RSA2048." } }, "pt-BR" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048." } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048." } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive 支持 EC256, EC384, RSA1024, 和RSA2048." } } @@ -3008,6 +3154,402 @@ } } }, + "export SSH_AUTH_SOCK=%@" : { + "shouldTranslate" : false + }, + "Host *\n\tIdentityAgent %@" : { + "shouldTranslate" : false + }, + "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_configure_using_secret_empty_create" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You'll need to create a Secret before configuring this action." + } + } + } + }, + "integrations_configure_using_secret_header" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure Using Secret" + } + } + } + }, + "integrations_configure_using_secret_no_secret" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Secret" + } + } + } + }, + "integrations_configure_using_secret_secret_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret" + } + } + } + }, + "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_git_step_gitallowedsigners_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/.gitallowedsigners probably does not exist. You'll need to create it." + } + } + } + }, + "integrations_git_step_gitconfig_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "[user]\n signingkey = %1$(publicKeyPathPlaceholder)@\n[commit]\n gpgsign = true\n[gpg]\n format = ssh\n[gpg \"ssh\"]\n allowedSignersFile = ~/.gitallowedsigners" + } + } + }, + "shouldTranslate" : false + }, + "integrations_menu_bar_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integrations…" + } + } + } + }, + "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_ssh_specific_key_note" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can tell SSH to use a specific key for a given host. See the web documentation for more details." + } + } + } + }, + "integrations_system_section_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "System" + } + } + } + }, + "integrations_tool_name_bash" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "bash" + } + } + }, + "shouldTranslate" : false + }, + "integrations_tool_name_fish" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "fish" + } + } + }, + "shouldTranslate" : false + }, + "integrations_tool_name_git_signing" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Git Signing" + } + } + } + }, + "integrations_tool_name_ssh" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH" + } + } + }, + "shouldTranslate" : false + }, + "integrations_tool_name_zsh" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "zsh" + } + } + }, + "shouldTranslate" : false + }, + "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" + } + } + } + }, + "integrationsGitStepGitconfigSectionNote" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If any section (like [user]) already exists, just add the entries in the existing section." + } + } + } + }, "no_secure_storage_description" : { "extractionState" : "manual", "localizations" : { @@ -3395,6 +3937,17 @@ } } }, + "reveal_in_finder_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reveal in Finder" + } + } + } + }, "secret_detail_md5_fingerprint_label" : { "extractionState" : "manual", "localizations" : { @@ -3892,6 +4445,9 @@ } } }, + "set -x SSH_AUTH_SOCK %@" : { + "shouldTranslate" : false + }, "setup_agent_activity_monitor_description" : { "extractionState" : "manual", "localizations" : { @@ -4176,150 +4732,46 @@ } } }, - "setup_ssh_add_for_me_button" : { + "setup_done_button" : { "extractionState" : "manual", "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegeix-ho per mi" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Für Mich Einfügen" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Add it For Me" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajoutez-le pour moi" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungila per me" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "自動で追加する" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "나를 위해 추가해주세요" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj za mnie" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicionar para mim" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить для меня" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "为我添加" + "value" : "Done" } } } }, - "setup_ssh_add_to_config_button" : { + "setup_integrations_button" : { "extractionState" : "manual", "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegeix a %1$(configPath)@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "In %1$(configPath)@ einfügen" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Add to %1$(configPath)@" + "value" : "Configure" } - }, - "fi" : { + } + } + }, + "setup_integrations_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Add to %1$(configPath)@" + "value" : "Tell the tools you use how to talk to Secretive." } - }, - "fr" : { + } + } + }, + "setup_integrations_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Ajouter à %1$(configPath)@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungi a %1$(configPath)@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$(configPath)@に追加" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$(configPath)@에 추가" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj do %1$(configPath)@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicionar para %1$(configPath)@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить к %1$(configPath)@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "添加到 %1$(configPath)@" + "value" : "Configure Integrations" } } } @@ -4395,290 +4847,6 @@ } } }, - "setup_ssh_description" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegeix aquesta línia a la teua configuració del shell per que SSH es comunique amb Secretive quan vulga autenticar. Secretive pot fer aquest procediment automàticament, o pots copiar i pegar açò al teu fitxer de configuració." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Füge diese Zeile in deine Shell-Konfiguration ein, damit SSH zur Authentifizierung mit dem Secret Agent kommuniziert. Secretive kann dies automatisch tun, oder du kopierst diese Zeile in deine Konfigurationsdatei." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajoutez cette ligne à votre configuration shell pour indiquer à SSH de communiquer à Secret Agent quand il veut s'authentifier. Secretive peut le faire automatiquement pour vous, ou vous pouvez copier et coller cette ligne dans votre fichier de configuration." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungi questa riga alla configurazione del Terminale per dire a SSH di parlare con Secret Agent quando vuole autenticarsi. Secretive può farlo automaticamente per te, oppure puoi copiare e incollare questa riga nel file di configurazione." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "以下の行をシェルの設定に追加してSSHが認証の際にSecretAgentを利用できるようにしてください。Secretiveが自動で追加するか、手動でコピーして設定に追加することもできます。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSH가 인증을 원할 때 Secret Agent와 통신하도록 지시하는 이 줄을 쉘 구성에 추가하세요. Secretive는 이 작업을 자동으로 수행하거나 사용자가 이를 복사하여 구성 파일에 붙여넣을 수 있습니다." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj tą linijkę to pliku konfiguracyjnego SSH, aby nawiązać połączenie z Secret Agent kiedy potrzebna jest autoryzacja. Secretive może ustawić to automatycznie lub możesz to zrobić samodzielnie kopiując to do pliku konfiguracyjnego." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicione esta linha nas configurações do seu shell para dizer ao SSH para falar com o Secret Agent quando ele necessitar de autenticação. Secretive pode fazer isto para você automaticamente ou você pode copiar e colar isso no seu arquivo de configuração." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавьте эту строчку к вашему конфигу shell, так SSH будет использовать SecretAgent в процессе аутентификации. Secretive может сделать это за Вас, либо Вы можете это скопировать сами." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "将以下文本添加到您的SSH 配置中以使用Secret Agent. Secretive 无法自动帮您完成该过程,或者您可以选择拷贝并粘贴该文本到您的配置文件中" - } - } - } - }, - "setup_ssh_title" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configura el teu agent SSH" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konfiguriere deinen SSH Agent" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure your SSH Agent" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurer votre Agent SSH" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configura il tuo Agente SSH" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSHエージェントを設定" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSH Agent 설정" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skonfiguruj twojego klienta SSH" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurar seu agente SSH" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Настройте Ваш SSH Agent" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "设置您的SSH 代理" - } - } - } - }, - "setup_step_complete_symbol" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - } - } - }, - "setup_third_party_faq_link" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si tractes de configurar una aplicació de tercers, comprova el FAQ." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schaue dir die FAQs an, um eine Drittanbieter-App einzurichten." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to set up a third party app, check out the FAQ." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si vous essayez de configurer une application tierce, consultez la FAQ." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se stai cercando di impostare un’app di terze parti, dai un'occhiata alla FAQ." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "その他のアプリから使う場合はよくある質問をご覧ください。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "타사 앱을 설정하려는 경우 FAQ를 확인하세요." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jeżeli próbujesz ustawić aplikacje stron trzecich, sprawdź FAQ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se você estiver tentando configurar um aplicativo de terceiros, verifique o FAQ." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Если Вы пытаетесь настроить сторонее приложение, ознакомьтесь с FAQ." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "如果您想设置第三方APP,请阅读 FAQ。" - } - } - } - }, "setup_updates_description" : { "extractionState" : "manual", "localizations" : { @@ -4750,7 +4918,7 @@ } } }, - "setup_updates_ok" : { + "setup_updates_ok_button" : { "extractionState" : "manual", "localizations" : { "ca" : { @@ -4963,6 +5131,17 @@ } } }, + "setupStepCompleteButton" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + } + } + }, "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.", "extractionState" : "manual", @@ -6135,6 +6314,17 @@ } } } + }, + "updater_download_latest_nightly_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download Latest Nightly Build" + } + } + } } }, "version" : "1.0" diff --git a/Sources/Packages/Sources/Brief/SemVer.swift b/Sources/Packages/Sources/Brief/SemVer.swift index 8df8c4a..472cd0e 100644 --- a/Sources/Packages/Sources/Brief/SemVer.swift +++ b/Sources/Packages/Sources/Brief/SemVer.swift @@ -5,12 +5,20 @@ public struct SemVer: Sendable { /// The SemVer broken into an array of integers. let versionNumbers: [Int] + public let previewDescription: String? + + public var isTestBuild: Bool { + versionNumbers == [0, 0, 0] + } /// Initializes a SemVer from a string representation. /// - Parameter version: A string representation of the SemVer, formatted as "major.minor.patch". public init(_ version: String) { // Betas have the format 1.2.3_beta1 - let strippedBeta = version.split(separator: "_").first! + // Nightlies have the format 0.0.0_nightly-2025-09-03 + let splitFull = version.split(separator: "_") + let strippedBeta = splitFull.first! + previewDescription = splitFull.count > 1 ? String(splitFull[1]) : nil var split = strippedBeta.split(separator: ".").compactMap { Int($0) } while split.count < 3 { split.append(0) @@ -22,6 +30,7 @@ public struct SemVer: Sendable { /// - Parameter version: An `OperatingSystemVersion` representation of the SemVer. public init(_ version: OperatingSystemVersion) { versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion] + previewDescription = nil } } diff --git a/Sources/Packages/Sources/Brief/Updater.swift b/Sources/Packages/Sources/Brief/Updater.swift index f79effe..600ddc5 100644 --- a/Sources/Packages/Sources/Brief/Updater.swift +++ b/Sources/Packages/Sources/Brief/Updater.swift @@ -13,12 +13,11 @@ import Observation state.update } - public let testBuild: Bool + /// The current version of the app that is running. + public let currentVersion: SemVer /// The current OS version. private let osVersion: SemVer - /// The current version of the app that is running. - private let currentVersion: SemVer /// Initializes an Updater. /// - Parameters: @@ -34,7 +33,6 @@ import Observation ) { self.osVersion = osVersion self.currentVersion = currentVersion - testBuild = currentVersion == SemVer("0.0.0") if checkOnLaunch { // Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet. Task { diff --git a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift index b9df10a..06c248e 100644 --- a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift +++ b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift @@ -5,8 +5,8 @@ public protocol UpdaterProtocol: Observable, Sendable { /// The latest update @MainActor var update: Release? { get } - /// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build) - var testBuild: Bool { get } + + var currentVersion: SemVer { get } func ignore(release: Release) async } diff --git a/Sources/Packages/Sources/Localization/Stub.swift b/Sources/Packages/Sources/Localization/Stub.swift deleted file mode 100644 index 8b13789..0000000 --- a/Sources/Packages/Sources/Localization/Stub.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift index 19b040a..892324f 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift @@ -133,20 +133,22 @@ private extension SocketPort { convenience init(path: String) { var addr = sockaddr_un() - addr.sun_family = sa_family_t(AF_UNIX) - var len: Int = 0 - withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in + let length = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in path.withCString { cstring in - len = strlen(cstring) + let len = strlen(cstring) strncpy(pointer, cstring, len) + return len } } - addr.sun_len = UInt8(len+2) + // This doesn't seem to be _strictly_ neccessary with SocketPort. + // but just for good form. + addr.sun_family = sa_family_t(AF_UNIX) + // This mirrors the SUN_LEN macro format. + addr.sun_len = UInt8(MemoryLayout.size - MemoryLayout.size(ofValue: addr.sun_path) + length) - var data: Data! - withUnsafePointer(to: &addr) { pointer in - data = Data(bytes: pointer, count: MemoryLayout.size) + let data = withUnsafePointer(to: &addr) { pointer in + Data(bytes: pointer, count: MemoryLayout.size) } self.init(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)! diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift index 2b16938..a5af72c 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -4,7 +4,7 @@ import OSLog /// Manages storage and lookup for OpenSSH certificates. public actor OpenSSHCertificateHandler: Sendable { - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let writer = OpenSSHPublicKeyWriter() private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] diff --git a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift index ada02d7..49983d2 100644 --- a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift +++ b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift @@ -5,12 +5,12 @@ import OSLog public final class PublicKeyFileStoreController: Sendable { private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController") - private let directory: String + private let directory: URL private let keyWriter = OpenSSHPublicKeyWriter() /// Initializes a PublicKeyFileStoreController. - public init(homeDirectory: String) { - directory = homeDirectory.appending("/PublicKeys") + public init(homeDirectory: URL) { + directory = homeDirectory.appending(component: "PublicKeys") } /// Writes out the keys specified to disk. @@ -20,16 +20,17 @@ public final class PublicKeyFileStoreController: Sendable { logger.log("Writing public keys to disk") if clear { let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) })) - let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? [] + let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? [] let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" } let untracked = Set(fullPathContents) .subtracting(validPaths) for path in untracked { - try? FileManager.default.removeItem(at: URL(fileURLWithPath: path)) + // string instead of fileURLWithPath since we're already using fileURL format. + try? FileManager.default.removeItem(at: URL(string: path)!) } } - try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil) + try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil) for secret in secrets { let path = publicKeyPath(for: secret) let data = Data(keyWriter.openSSHString(secret: secret).utf8) @@ -44,14 +45,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. public func publicKeyPath(for secret: SecretType) -> String { let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return directory.appending("/").appending("\(minimalHex).pub") + return directory.appending(component: "\(minimalHex).pub").path() } /// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory. public var hasAnyCertificates: Bool { do { return try FileManager.default - .contentsOfDirectory(atPath: directory) + .contentsOfDirectory(atPath: directory.path()) .filter { $0.hasSuffix("-cert.pub") } .isEmpty == false } catch { @@ -65,7 +66,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. public func sshCertificatePath(for secret: SecretType) -> String { let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return directory.appending("/").appending("\(minimalHex)-cert.pub") + return directory.appending(component: "\(minimalHex)-cert.pub").path() } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift index ddcc042..a4c69d2 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift @@ -30,7 +30,7 @@ extension SecureEnclave { SecItemCopyMatching(privateAttributes, &privateUntyped) guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return } let migratedPublicKeys = Set(store.secrets.map(\.publicKey)) - var migrated = false + var migratedAny = false for key in privateTyped { let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret) let id = key[kSecAttrApplicationLabel] as! Data @@ -45,20 +45,24 @@ extension SecureEnclave { // Best guess. let auth: AuthenticationRequirement = String(describing: accessControl) .contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown - let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) - let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) - guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else { - logger.log("Skipping \(name), public key already present. Marking as migrated.") + do { + let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) + let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) + guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else { + logger.log("Skipping \(name), public key already present. Marking as migrated.") + try markMigrated(secret: secret, oldID: id) + continue + } + logger.log("Migrating \(name).") + try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes) + logger.log("Migrated \(name).") try markMigrated(secret: secret, oldID: id) - continue + migratedAny = true + } catch { + logger.error("Failed to migrate \(name): \(error).") } - logger.log("Migrating \(name).") - try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes) - logger.log("Migrated \(name).") - try markMigrated(secret: secret, oldID: id) - migrated = true } - if migrated { + if migratedAny { store.reloadSecrets() } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index c57b712..433759d 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -26,7 +26,7 @@ extension SecureEnclave { for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { guard Constants.notificationToken != (note.object as? String) else { // Don't reload if we're the ones triggering this by reloading. - return + continue } reloadSecrets() } @@ -112,7 +112,7 @@ extension SecureEnclave { var accessError: SecurityError? let flags: SecAccessControlCreateFlags = switch attributes.authentication { case .notRequired: - [] + [.privateKeyUsage] case .presenceRequired: [.userPresence, .privateKeyUsage] case .biometryCurrent: diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 5800c75..ee4b799 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -21,7 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() private let updater = Updater(checkOnLaunch: true) private let notifier = Notifier() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) private lazy var agent: Agent = { Agent(storeList: storeList, witness: notifier) }() @@ -58,7 +58,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { updater.update } onChange: { [updater, notifier] in Task { - guard !updater.testBuild else { return } + guard !updater.currentVersion.isTestBuild else { return } await notifier.notify(update: updater.update!) { release in await updater.ignore(release: release) } diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 459af4a..96c6479 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -26,6 +26,11 @@ 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; + 504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; }; + 504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; }; + 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; }; + 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; }; + 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; @@ -36,7 +41,6 @@ 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; }; 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.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 */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; }; @@ -49,8 +53,12 @@ 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; }; 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; }; 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 */; }; 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 */; }; 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; /* End PBXBuildFile section */ @@ -103,10 +111,15 @@ 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = ""; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = ""; }; - 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Localizable.xcstrings; sourceTree = SOURCE_ROOT; }; + 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = ""; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; + 504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; + 504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; }; + 504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = ""; }; + 504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = ""; }; + 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = ""; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = ""; }; 50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = ""; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -120,7 +133,6 @@ 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = ""; }; 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = ""; }; - 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = ""; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = ""; }; 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = ""; }; @@ -138,8 +150,12 @@ 50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = ""; }; + 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = ""; }; 50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = ""; }; 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = ""; }; + 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = ""; }; + 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = ""; }; + 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = ""; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = ""; }; 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -179,6 +195,56 @@ path = Helpers; sourceTree = ""; }; + 504788ED2E681EB200B4556F /* Styles */ = { + isa = PBXGroup; + children = ( + 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */, + 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */, + 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */, + 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, + ); + path = Styles; + sourceTree = ""; + }; + 504788EE2E681EC300B4556F /* Secrets */ = { + isa = PBXGroup; + children = ( + 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */, + 50B8550C24138C4F009958AC /* DeleteSecretView.swift */, + 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */, + 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, + 506772C82425BB8500034DED /* NoStoresView.swift */, + 50C385A42407A76D00AF2719 /* SecretDetailView.swift */, + 50153E21250DECA300525160 /* SecretListItemView.swift */, + 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, + ); + path = Secrets; + sourceTree = ""; + }; + 504788EF2E681ED700B4556F /* Configuration */ = { + isa = PBXGroup; + children = ( + 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */, + 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */, + 504788F12E681F3A00B4556F /* Instructions.swift */, + 504788F32E681F6900B4556F /* ToolConfigurationView.swift */, + 5066A6C12516F303004B5A36 /* SetupView.swift */, + 504788F52E68206F00B4556F /* GettingStartedView.swift */, + ); + path = Configuration; + sourceTree = ""; + }; + 504788F02E681F0100B4556F /* Views */ = { + isa = PBXGroup; + children = ( + 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, + 50617D8423FCE48E0099B055 /* ContentView.swift */, + 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, + 50153E1F250AFCB200525160 /* UpdateView.swift */, + ); + path = Views; + sourceTree = ""; + }; 50617D7623FCE48D0099B055 = { isa = PBXGroup; children = ( @@ -241,20 +307,10 @@ 508A58B0241ED1C40069DC07 /* Views */ = { isa = PBXGroup; children = ( - 50617D8423FCE48E0099B055 /* ContentView.swift */, - 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, - 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */, - 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, - 50153E21250DECA300525160 /* SecretListItemView.swift */, - 50C385A42407A76D00AF2719 /* SecretDetailView.swift */, - 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */, - 50B8550C24138C4F009958AC /* DeleteSecretView.swift */, - 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */, - 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, - 506772C82425BB8500034DED /* NoStoresView.swift */, - 50153E1F250AFCB200525160 /* UpdateView.swift */, - 5066A6C12516F303004B5A36 /* SetupView.swift */, - 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, + 504788EF2E681ED700B4556F /* Configuration */, + 504788EE2E681EC300B4556F /* Secrets */, + 504788ED2E681EB200B4556F /* Styles */, + 504788F02E681F0100B4556F /* Views */, ); path = Views; sourceTree = ""; @@ -262,11 +318,11 @@ 508A58B1241ED1EA0069DC07 /* Controllers */ = { isa = PBXGroup; children = ( + 504788EB2E680DC400B4556F /* URLs.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */, - 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */, ); path = Controllers; sourceTree = ""; @@ -433,26 +489,34 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 504788F22E681F3A00B4556F /* Instructions.swift in Sources */, + 50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, + 504788EC2E680DC800B4556F /* URLs.swift in Sources */, + 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, + 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */, 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, - 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */, 50033AC327813F1700253856 /* BundleIDs.swift in Sources */, + 50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, + 50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, + 50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */, + 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */, 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, @@ -647,10 +711,18 @@ ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_PREVIEWS = YES; - ENABLE_USER_SELECTED_FILES = readwrite; + 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 = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -679,10 +751,18 @@ ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_PREVIEWS = YES; - ENABLE_USER_SELECTED_FILES = readwrite; + 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 = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -783,10 +863,18 @@ ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; ENABLE_HARDENED_RUNTIME = NO; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_PREVIEWS = YES; - ENABLE_USER_SELECTED_FILES = readwrite; + 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 = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -809,8 +897,17 @@ DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = 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; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -835,8 +932,17 @@ DEVELOPMENT_TEAM = Z72PRUAWF6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = 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; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -862,8 +968,17 @@ DEVELOPMENT_TEAM = Z72PRUAWF6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = 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; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 177beaf..64cc298 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -25,6 +25,9 @@ extension EnvironmentValues { }() @Entry var updater: any UpdaterProtocol = _updater + private static let _justUpdatedChecker = JustUpdatedChecker() + @Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker + @MainActor var secretStoreList: SecretStoreList { EnvironmentValues._secretStoreList } @@ -33,10 +36,11 @@ extension EnvironmentValues { @main struct Secretive: App { - private let justUpdatedChecker = JustUpdatedChecker() @Environment(\.agentStatusChecker) var agentStatusChecker + @Environment(\.justUpdatedChecker) var justUpdatedChecker @AppStorage("defaultsHasRunSetup") var hasRunSetup = false @State private var showingSetup = false + @State private var showingIntegrations = false @State private var showingCreation = false @SceneBuilder var body: some Scene { @@ -51,15 +55,23 @@ struct Secretive: App { .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in guard hasRunSetup else { return } agentStatusChecker.check() - if agentStatusChecker.running && justUpdatedChecker.justUpdated { + if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild { // Relaunch the agent, since it'll be running from earlier update still reinstallAgent() } else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild { forceLaunchAgent() } } + .sheet(isPresented: $showingIntegrations) { + IntegrationsView() + } } .commands { + CommandGroup(before: CommandGroupPlacement.appSettings) { + Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") { + showingIntegrations = true + } + } CommandGroup(after: CommandGroupPlacement.newItem) { Button(.appMenuNewSecretButton) { showingCreation = true @@ -71,11 +83,6 @@ struct Secretive: App { NSWorkspace.shared.open(Constants.helpURL) } } - CommandGroup(after: .help) { - Button(.appMenuSetupButton) { - showingSetup = true - } - } SidebarCommands() } } @@ -85,9 +92,8 @@ struct Secretive: App { extension Secretive { private func reinstallAgent() { - justUpdatedChecker.check() Task { - await LaunchAgentController().install() + _ = await LaunchAgentController().install() try? await Task.sleep(for: .seconds(1)) agentStatusChecker.check() if !agentStatusChecker.running { diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index 3c85f3f..b7327a6 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -6,12 +6,14 @@ import Observation @MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { var running: Bool { get } var developmentBuild: Bool { get } + var process: NSRunningApplication? { get } func check() } @Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { var running: Bool = false + var process: NSRunningApplication? = nil nonisolated init() { Task { @MainActor in @@ -20,32 +22,39 @@ import Observation } func check() { - running = instanceSecretAgentProcess != nil + process = instanceSecretAgentProcess + running = process != nil } // All processes, including ones from older versions, etc - var secretAgentProcesses: [NSRunningApplication] { - NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID) + var allSecretAgentProcesses: [NSRunningApplication] { + NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.agentBundleID) } // The process corresponding to this instance of Secretive var instanceSecretAgentProcess: NSRunningApplication? { - let agents = secretAgentProcesses + // FIXME: CHECK VERSION + let agents = allSecretAgentProcesses for agent in agents { guard let url = agent.bundleURL else { continue } - if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) { + if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) { return agent } } return nil } - // Whether Secretive is being run in an Xcode environment. var developmentBuild: Bool { - Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode") + Bundle.main.bundleURL.isXcodeURL } } +extension URL { + var isXcodeURL: Bool { + absoluteString.contains("/Library/Developer/Xcode") + } + +} diff --git a/Sources/Secretive/Controllers/JustUpdatedChecker.swift b/Sources/Secretive/Controllers/JustUpdatedChecker.swift index 7b25eef..75e9483 100644 --- a/Sources/Secretive/Controllers/JustUpdatedChecker.swift +++ b/Sources/Secretive/Controllers/JustUpdatedChecker.swift @@ -1,23 +1,33 @@ import Foundation import AppKit -protocol JustUpdatedCheckerProtocol: Observable { - var justUpdated: Bool { get } +@MainActor protocol JustUpdatedCheckerProtocol: Observable { + var justUpdatedBuild: Bool { get } + var justUpdatedOS: Bool { get } } -@Observable class JustUpdatedChecker: JustUpdatedCheckerProtocol { +@Observable @MainActor class JustUpdatedChecker: JustUpdatedCheckerProtocol { - var justUpdated: Bool = false + var justUpdatedBuild: Bool = false + var justUpdatedOS: Bool = false - init() { - check() + nonisolated init() { + Task { @MainActor in + check() + } } - func check() { - let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None" + private func check() { + let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String + let lastOS = UserDefaults.standard.object(forKey: Constants.previousOSVersionUserDefaultsKey) as? String let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String + let osRaw = ProcessInfo.processInfo.operatingSystemVersion + let currentOS = "\(osRaw.majorVersion).\(osRaw.minorVersion).\(osRaw.patchVersion)" UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey) - justUpdated = lastBuild != currentBuild + UserDefaults.standard.set(currentOS, forKey: Constants.previousOSVersionUserDefaultsKey) + justUpdatedBuild = lastBuild != currentBuild + // To prevent this showing on first lauch for every user, only show if lastBuild is non-nil. + justUpdatedOS = lastBuild != nil && lastOS != currentOS } @@ -28,6 +38,7 @@ extension JustUpdatedChecker { enum Constants { static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild" + static let previousOSVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastOS" } } diff --git a/Sources/Secretive/Controllers/LaunchAgentController.swift b/Sources/Secretive/Controllers/LaunchAgentController.swift index a65f8b0..308c381 100644 --- a/Sources/Secretive/Controllers/LaunchAgentController.swift +++ b/Sources/Secretive/Controllers/LaunchAgentController.swift @@ -8,16 +8,28 @@ struct LaunchAgentController { private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController") - func install() async { + func install() async -> Bool { logger.debug("Installing agent") _ = setEnabled(false) // 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 // and start new? try? await Task.sleep(for: .seconds(1)) - await MainActor.run { - _ = setEnabled(true) + let result = await MainActor.run { + 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 { @@ -28,6 +40,7 @@ struct LaunchAgentController { do { try await NSWorkspace.shared.openApplication(at: url, configuration: config) logger.debug("Agent force launched") + try? await Task.sleep(for: .seconds(1)) return true } catch { logger.error("Error force launching \(error.localizedDescription)") @@ -36,7 +49,7 @@ struct LaunchAgentController { } private func setEnabled(_ enabled: Bool) -> Bool { - let service = SMAppService.loginItem(identifier: Bundle.main.agentBundleID) + let service = SMAppService.loginItem(identifier: Bundle.agentBundleID) do { if enabled { try service.register() diff --git a/Sources/Secretive/Controllers/ShellConfigurationController.swift b/Sources/Secretive/Controllers/ShellConfigurationController.swift deleted file mode 100644 index 2ecb17e..0000000 --- a/Sources/Secretive/Controllers/ShellConfigurationController.swift +++ /dev/null @@ -1,63 +0,0 @@ -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 - } - -} diff --git a/Sources/Secretive/Controllers/URLs.swift b/Sources/Secretive/Controllers/URLs.swift new file mode 100644 index 0000000..3ea1fe5 --- /dev/null +++ b/Sources/Secretive/Controllers/URLs.swift @@ -0,0 +1,25 @@ +import Foundation + +extension URL { + + static var agentHomeURL: URL { + URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID)) + } + + static var socketPath: String { + URL.agentHomeURL.appendingPathComponent("socket.ssh").path() + } + +} + +extension String { + + var normalizedPathAndFolder: (String, String) { + // All foundation-based normalization methods replace this with the container directly. + let processedPath = replacingOccurrences(of: "~", with: "/Users/\(NSUserName())") + let url = URL(filePath: processedPath) + let folder = url.deletingLastPathComponent().path() + return (processedPath, folder) + } + +} diff --git a/Sources/Secretive/Helpers/BundleIDs.swift b/Sources/Secretive/Helpers/BundleIDs.swift index de4967d..bc84add 100644 --- a/Sources/Secretive/Helpers/BundleIDs.swift +++ b/Sources/Secretive/Helpers/BundleIDs.swift @@ -1,7 +1,11 @@ import Foundation - extension Bundle { - public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!} - public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!} + public static var agentBundleID: String { + Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent") + } + + public static var hostBundleID: String { + Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host") + } } diff --git a/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift b/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift index 51a5c09..e9799e9 100644 --- a/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift +++ b/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift @@ -1,12 +1,15 @@ import Foundation +import AppKit class PreviewAgentStatusChecker: AgentStatusCheckerProtocol { let running: Bool + let process: NSRunningApplication? let developmentBuild = false - init(running: Bool = true) { + init(running: Bool = true, process: NSRunningApplication? = nil) { self.running = running + self.process = process } func check() { diff --git a/Sources/Secretive/Preview Content/PreviewUpdater.swift b/Sources/Secretive/Preview Content/PreviewUpdater.swift index 77fbeea..b8bc29a 100644 --- a/Sources/Secretive/Preview Content/PreviewUpdater.swift +++ b/Sources/Secretive/Preview Content/PreviewUpdater.swift @@ -6,7 +6,7 @@ import Brief var update: Release? = nil - let testBuild = false + let currentVersion = SemVer("0.0.0_preview") init(update: Update = .none) { switch update { diff --git a/Sources/Secretive/Views/ActionButtonStyle.swift b/Sources/Secretive/Views/ActionButtonStyle.swift deleted file mode 100644 index 4d7455f..0000000 --- a/Sources/Secretive/Views/ActionButtonStyle.swift +++ /dev/null @@ -1,24 +0,0 @@ -import SwiftUI - -struct PrimaryButtonModifier: ViewModifier { - - @Environment(\.colorScheme) var colorScheme - - func body(content: Content) -> some View { - // Tinted glass prominent is really hard to read on 26.0. - if #available(macOS 26.0, *), colorScheme == .dark { - content.buttonStyle(.glassProminent) - } else { - content.buttonStyle(.borderedProminent) - } - } - -} - -extension View { - - func primary() -> some View { - modifier(PrimaryButtonModifier()) - } - -} diff --git a/Sources/Secretive/Views/Configuration/ConfigurationItemView.swift b/Sources/Secretive/Views/Configuration/ConfigurationItemView.swift new file mode 100644 index 0000000..77e6a2e --- /dev/null +++ b/Sources/Secretive/Views/Configuration/ConfigurationItemView.swift @@ -0,0 +1,56 @@ +import SwiftUI + +struct ConfigurationItemView: 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(.copyableClickToCopyButton, 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") { + let (processedPath, folder) = rawPath.normalizedPathAndFolder + NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder) + } + .labelStyle(.iconOnly) + .buttonStyle(.borderless) + case nil: + EmptyView() + } + } + content + } + } +} + diff --git a/Sources/Secretive/Views/Configuration/GettingStartedView.swift b/Sources/Secretive/Views/Configuration/GettingStartedView.swift new file mode 100644 index 0000000..67c7b42 --- /dev/null +++ b/Sources/Secretive/Views/Configuration/GettingStartedView.swift @@ -0,0 +1,49 @@ +import SwiftUI + +struct GettingStartedView: View { + + private let instructions = Instructions() + + @Binding var selectedInstruction: ConfigurationFileInstructions? + + init(selectedInstruction: Binding) { + _selectedInstruction = selectedInstruction + } + + var body: some View { + Form { + Section(.integrationsGettingStartedTitle) { + Text(.integrationsGettingStartedTitleDescription) + } + Section { + Group { + Text(.integrationsGettingStartedSuggestionSsh) + .onTapGesture { + self.selectedInstruction = instructions.ssh + } + VStack(alignment: .leading, spacing: 5) { + Text(.integrationsGettingStartedSuggestionShell) + Text(.integrationsGettingStartedSuggestionShellDefault(shellName: String(localized: 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) + } + +} diff --git a/Sources/Secretive/Views/Configuration/Instructions.swift b/Sources/Secretive/Views/Configuration/Instructions.swift new file mode 100644 index 0000000..bb92b86 --- /dev/null +++ b/Sources/Secretive/Views/Configuration/Instructions.swift @@ -0,0 +1,179 @@ +import Foundation + +struct Instructions { + + enum Constants { + static let publicKeyPathPlaceholder = "_PUBLIC_KEY_PATH_PLACEHOLDER_" + static let publicKeyPlaceholder = "_PUBLIC_KEY_PLACEHOLDER_" + } + + var defaultShell: ConfigurationFileInstructions { + zsh + } + + var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted) + + var ssh: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: LocalizedStringResource.integrationsToolNameSsh, + configPath: "~/.ssh/config", + configText: "Host *\n\tIdentityAgent \(URL.socketPath)", + website: URL(string: "https://man.openbsd.org/ssh_config.5")!, + note: .integrationsSshSpecificKeyNote, + ) + } + + var git: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: .integrationsToolNameGitSigning, + steps: [ + .init(path: "~/.gitconfig", steps: [ + .integrationsGitStepGitconfigDescription(publicKeyPathPlaceholder: Constants.publicKeyPathPlaceholder) + ], + note: .integrationsGitStepGitconfigSectionNote + ), + .init( + path: "~/.gitallowedsigners", + steps: [ + LocalizedStringResource(stringLiteral: Constants.publicKeyPlaceholder) + ], + note: .integrationsGitStepGitallowedsignersDescription + ), + ], + website: URL(string: "https://git-scm.com/docs/git-config")!, + ) + } + + var zsh: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: .integrationsToolNameZsh, + configPath: "~/.zshrc", + configText: "export SSH_AUTH_SOCK=\(URL.socketPath)" + ) + } + + var instructions: [ConfigurationGroup] { + [ + ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [ + gettingStarted + ]), + ConfigurationGroup( + name: .integrationsSystemSectionTitle, + instructions: [ + ssh, + git, + ] + ), + ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [ + zsh, + ConfigurationFileInstructions( + tool: .integrationsToolNameBash, + configPath: "~/.bashrc", + configText: "export SSH_AUTH_SOCK=\(URL.socketPath)" + ), + ConfigurationFileInstructions( + tool: .integrationsToolNameFish, + configPath: "~/.config/fish/config.fish", + configText: "set -x SSH_AUTH_SOCK \(URL.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: [LocalizedStringResource] + let note: LocalizedStringResource? + var id: String { path } + + init(path: String, steps: [LocalizedStringResource], note: LocalizedStringResource? = nil) { + self.path = path + self.steps = steps + self.note = note + } + + func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + } + } + + var id: ID + var tool: LocalizedStringResource + var steps: [StepGroup] + var requiresSecret: Bool + var website: URL? + + init( + tool: LocalizedStringResource, + configPath: String, + configText: LocalizedStringResource, + requiresSecret: Bool = false, + website: URL? = nil, + note: LocalizedStringResource? = nil + ) { + self.id = .tool(String(localized: tool)) + self.tool = tool + self.steps = [StepGroup(path: configPath, steps: [configText], note: note)] + self.requiresSecret = requiresSecret + self.website = website + } + + init( + tool: LocalizedStringResource, + steps: [StepGroup], + requiresSecret: Bool = false, + website: URL? = nil + ) { + self.id = .tool(String(localized: tool)) + self.tool = tool + self.steps = steps + self.requiresSecret = true + self.website = website + } + + init(_ name: LocalizedStringResource, id: ID) { + self.id = id + tool = name + steps = [] + requiresSecret = false + } + + func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + } + + 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" + } + } + } + +} diff --git a/Sources/Secretive/Views/Configuration/IntegrationsView.swift b/Sources/Secretive/Views/Configuration/IntegrationsView.swift new file mode 100644 index 0000000..de6b8a0 --- /dev/null +++ b/Sources/Secretive/Views/Configuration/IntegrationsView.swift @@ -0,0 +1,115 @@ +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: () -> Content) -> some View { + modifier(FauxToolbarModifier(toolbarContent: content())) + } + +} + +struct FauxToolbarModifier: ViewModifier { + + var toolbarContent: ToolbarContent + + func body(content: Content) -> some View { + VStack(alignment: .leading, spacing: 0) { + content + Divider() + HStack { + Spacer() + toolbarContent + .padding(.top, 8) + .padding(.trailing, 16) + .padding(.bottom, 16) + } + } + + } + +} + +struct IntegrationsDetailView: View { + + @Binding private var selectedInstruction: ConfigurationFileInstructions? + + init(selectedInstruction: Binding) { + _selectedInstruction = selectedInstruction + } + + var body: some View { + if let selectedInstruction { + switch selectedInstruction.id { + case .gettingStarted: + GettingStartedView(selectedInstruction: $selectedInstruction) + case .tool: + ToolConfigurationView(selectedInstruction: selectedInstruction) + 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) + } + } + + } + +} + +#Preview { + IntegrationsView() + .frame(height: 500) +} diff --git a/Sources/Secretive/Views/Configuration/SetupView.swift b/Sources/Secretive/Views/Configuration/SetupView.swift new file mode 100644 index 0000000..2c2d66e --- /dev/null +++ b/Sources/Secretive/Views/Configuration/SetupView.swift @@ -0,0 +1,187 @@ +import SwiftUI + +struct SetupView: View { + + @Environment(\.dismiss) private var dismiss + @Binding var setupComplete: Bool + + @State var showingIntegrations = false + @State var buttonWidth: CGFloat? + + @State var installed = false + @State var updates = false + @State var integrations = false + var allDone: Bool { + installed && updates && integrations + } + + var body: some View { + VStack { + VStack(alignment: .leading, spacing: 0) { + StepView( + title: .setupAgentTitle, + description: .setupAgentDescription, + 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: 600, 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: 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: 0) { + icon + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24) + Spacer() + .frame(width: 20) + VStack(alignment: .leading, spacing: 4) { + Text(title) + .bold() + Text(description) + } + Spacer(minLength: 20) + actions + } + .padding(20) + } + +} + +extension SetupView { + + enum Constants { + static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")! + } + +} + +#Preview { + SetupView(setupComplete: .constant(false)) +} diff --git a/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift b/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift new file mode 100644 index 0000000..cd1bc69 --- /dev/null +++ b/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift @@ -0,0 +1,110 @@ +import SwiftUI +import SecretKit + +struct ToolConfigurationView: View { + + private let instructions = Instructions() + let selectedInstruction: ConfigurationFileInstructions + + @Environment(\.secretStoreList) private var secretStoreList + + @State var creating = false + @State var selectedSecret: AnySecret? + + init(selectedInstruction: ConfigurationFileInstructions) { + self.selectedInstruction = selectedInstruction + } + + var body: some View { + Form { + if selectedInstruction.requiresSecret { + if secretStoreList.allSecrets.isEmpty { + Section { + Text(.integrationsConfigureUsingSecretEmptyCreate) + if let store = secretStoreList.modifiableStore { + HStack { + Spacer() + Button(.createSecretTitle) { + creating = true + } + .sheet(isPresented: $creating) { + CreateSecretView(store: store) { created in + selectedSecret = created + } + } + } + } + } + } else { + Section { + Picker(.integrationsConfigureUsingSecretSecretTitle, selection: $selectedSecret) { + if selectedSecret == nil { + Text(.integrationsConfigureUsingSecretNoSecret) + .tag(nil as (AnySecret?)) + } + ForEach(secretStoreList.allSecrets) { secret in + Text(secret.name) + .tag(secret) + } + } + } header: { + Text(.integrationsConfigureUsingSecretHeader) + } + .onAppear { + selectedSecret = secretStoreList.allSecrets.first + } + } + } + ForEach(selectedInstruction.steps) { stepGroup in + Section { + ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path)) + ForEach(stepGroup.steps, id: \.self.key) { step in + ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) { + HStack { + Text(placeholdersReplaced(text: String(localized: 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) + + } + + func placeholdersReplaced(text: String) -> String { + guard let selectedSecret else { return text } + let writer = OpenSSHPublicKeyWriter() + let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) + return text + .replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret)) + .replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret)) + } + +} diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/Secrets/CreateSecretView.swift similarity index 81% rename from Sources/Secretive/Views/CreateSecretView.swift rename to Sources/Secretive/Views/Secrets/CreateSecretView.swift index b5f17b5..78fd4e6 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/Secrets/CreateSecretView.swift @@ -4,13 +4,15 @@ import SecretKit struct CreateSecretView: View { @State var store: StoreType - @Binding var showing: Bool + @Environment(\.dismiss) private var dismiss + var createdSecret: (AnySecret?) -> Void @State private var name = "" @State private var keyAttribution = "" @State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired @State private var keyType: KeyType? @State var advanced = false + @State var errorText: String? private var authenticationOptions: [AuthenticationRequirement] { if advanced || authenticationRequirement == .biometryCurrent { @@ -64,7 +66,7 @@ struct CreateSecretView: View { Text(.createSecretBiometryCurrentWarning) .padding(.horizontal, 10) .padding(.vertical, 3) - .background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5)) + .boxBackground(color: .red) } } @@ -83,7 +85,7 @@ struct CreateSecretView: View { Text(.createSecretMldsaWarning) .padding(.horizontal, 10) .padding(.vertical, 3) - .background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5)) + .boxBackground(color: .orange) } } VStack(alignment: .leading) { @@ -94,16 +96,24 @@ struct CreateSecretView: View { } } } + if let errorText { + Section { + } footer: { + Text(verbatim: errorText) + .errorStyle() + } + } } HStack { Toggle(.createSecretAdvancedLabel, isOn: $advanced) .toggleStyle(.button) Spacer() Button(.createSecretCancelButton, role: .cancel) { - showing = false + dismiss() } Button(.createSecretCreateButton, action: save) - .primary() + .keyboardShortcut(.return) + .primaryButton() .disabled(name.isEmpty) } .padding() @@ -117,20 +127,25 @@ struct CreateSecretView: View { func save() { let attribution = keyAttribution.isEmpty ? nil : keyAttribution Task { - try! await store.create( - name: name, - attributes: .init( - keyType: keyType!, - authentication: authenticationRequirement, - publicKeyAttribution: attribution + do { + let new = try await store.create( + name: name, + attributes: .init( + keyType: keyType!, + authentication: authenticationRequirement, + publicKeyAttribution: attribution + ) ) - ) - showing = false + createdSecret(AnySecret(new)) + dismiss() + } catch { + errorText = error.localizedDescription + } } } } -#Preview { - CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) -} +//#Preview { +// CreateSecretView(store: Preview.StoreModifiable()) { _ in } +//} diff --git a/Sources/Secretive/Views/DeleteSecretView.swift b/Sources/Secretive/Views/Secrets/DeleteSecretView.swift similarity index 95% rename from Sources/Secretive/Views/DeleteSecretView.swift rename to Sources/Secretive/Views/Secrets/DeleteSecretView.swift index 2deee63..17f6610 100644 --- a/Sources/Secretive/Views/DeleteSecretView.swift +++ b/Sources/Secretive/Views/Secrets/DeleteSecretView.swift @@ -28,8 +28,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier { TextField(secret.name, text: $confirmedSecretName) if let errorText { Text(verbatim: errorText) - .foregroundStyle(.red) - .font(.callout) + .errorStyle() } Button(.deleteConfirmationDeleteButton, action: delete) .disabled(confirmedSecretName != secret.name) diff --git a/Sources/Secretive/Views/EditSecretView.swift b/Sources/Secretive/Views/Secrets/EditSecretView.swift similarity index 90% rename from Sources/Secretive/Views/EditSecretView.swift rename to Sources/Secretive/Views/Secrets/EditSecretView.swift index cdc4114..80f5af0 100644 --- a/Sources/Secretive/Views/EditSecretView.swift +++ b/Sources/Secretive/Views/Secrets/EditSecretView.swift @@ -30,21 +30,22 @@ struct EditSecretView: View { .font(.subheadline) .foregroundStyle(.secondary) } - } - if let errorText { - Text(verbatim: errorText) - .foregroundStyle(.red) - .font(.callout) + } footer: { + if let errorText { + Text(verbatim: errorText) + .errorStyle() + } } } HStack { - Button(.editSaveButton, action: rename) - .disabled(name.isEmpty) - .keyboardShortcut(.return) Button(.editCancelButton) { dismissalBlock(false) } .keyboardShortcut(.cancelAction) + Button(.editSaveButton, action: rename) + .disabled(name.isEmpty) + .keyboardShortcut(.return) + .primaryButton() } .padding() } diff --git a/Sources/Secretive/Views/EmptyStoreView.swift b/Sources/Secretive/Views/Secrets/EmptyStoreView.swift similarity index 70% rename from Sources/Secretive/Views/EmptyStoreView.swift rename to Sources/Secretive/Views/Secrets/EmptyStoreView.swift index 3f0bd81..c21cf95 100644 --- a/Sources/Secretive/Views/EmptyStoreView.swift +++ b/Sources/Secretive/Views/Secrets/EmptyStoreView.swift @@ -27,7 +27,9 @@ struct EmptyStoreImmutableView: View { } struct EmptyStoreModifiableView: View { - + + @Environment(\.justUpdatedChecker) var justUpdatedChecker + var body: some View { GeometryReader { windowGeometry in VStack { @@ -51,21 +53,35 @@ struct EmptyStoreModifiableView: View { }.frame(height: (windowGeometry.size.height/2) - 20).padding() Text(.emptyStoreModifiableClickHereTitle).bold() Text(.emptyStoreModifiableClickHereDescription) + if justUpdatedChecker.justUpdatedOS { + Spacer() + .frame(height: 20) + VStack(spacing: 10) { + Text(.emptyStoreModifiableEmptyOsWarningTitle) + .font(.title2) + .bold() + Text(.emptyStoreModifiableEmptyOsWarningDescription) + .fixedSize(horizontal: false, vertical: true) + .bold() + } + .padding() + .boxBackground(color: .orange) + .padding() + } Spacer() }.frame(maxWidth: .infinity, maxHeight: .infinity) } } } -#if DEBUG -struct EmptyStoreModifiableView_Previews: PreviewProvider { - static var previews: some View { - Group { - EmptyStoreImmutableView() - EmptyStoreModifiableView() - } - } +#Preview { + EmptyStoreImmutableView() +} +#Preview { + EmptyStoreImmutableView() +// .environment(\.justUpdatedChecker, <#T##value: V##V#>) +} +#Preview { + EmptyStoreModifiableView() } - -#endif diff --git a/Sources/Secretive/Views/NoStoresView.swift b/Sources/Secretive/Views/Secrets/NoStoresView.swift similarity index 73% rename from Sources/Secretive/Views/NoStoresView.swift rename to Sources/Secretive/Views/Secrets/NoStoresView.swift index 497138d..fd31ddf 100644 --- a/Sources/Secretive/Views/NoStoresView.swift +++ b/Sources/Secretive/Views/Secrets/NoStoresView.swift @@ -13,12 +13,7 @@ struct NoStoresView: View { } -#if DEBUG - -struct NoStoresView_Previews: PreviewProvider { - static var previews: some View { - NoStoresView() - } +#Preview { + NoStoresView() } -#endif diff --git a/Sources/Secretive/Views/SecretDetailView.swift b/Sources/Secretive/Views/Secrets/SecretDetailView.swift similarity index 78% rename from Sources/Secretive/Views/SecretDetailView.swift rename to Sources/Secretive/Views/Secrets/SecretDetailView.swift index 68a1e05..b3940ff 100644 --- a/Sources/Secretive/Views/SecretDetailView.swift +++ b/Sources/Secretive/Views/Secrets/SecretDetailView.swift @@ -6,8 +6,8 @@ struct SecretDetailView: View { let secret: SecretType private let keyWriter = OpenSSHPublicKeyWriter() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID)) - + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) + var body: some View { ScrollView { Form { @@ -21,7 +21,7 @@ struct SecretDetailView: View { CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString) Spacer() .frame(height: 20) - CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret)) + CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret), showRevealInFinder: true) Spacer() } } @@ -37,12 +37,6 @@ struct SecretDetailView: View { } -#if DEBUG - -struct SecretDetailView_Previews: PreviewProvider { - static var previews: some View { - SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0]) - } -} - -#endif +//#Preview { +// SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret")) +//} diff --git a/Sources/Secretive/Views/SecretListItemView.swift b/Sources/Secretive/Views/Secrets/SecretListItemView.swift similarity index 100% rename from Sources/Secretive/Views/SecretListItemView.swift rename to Sources/Secretive/Views/Secrets/SecretListItemView.swift diff --git a/Sources/Secretive/Views/StoreListView.swift b/Sources/Secretive/Views/Secrets/StoreListView.swift similarity index 100% rename from Sources/Secretive/Views/StoreListView.swift rename to Sources/Secretive/Views/Secrets/StoreListView.swift diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift deleted file mode 100644 index e0a2560..0000000 --- a/Sources/Secretive/Views/SetupView.swift +++ /dev/null @@ -1,297 +0,0 @@ -import SwiftUI - -struct SetupView: View { - - @State var stepIndex = 0 - @Binding var visible: Bool - @Binding var setupComplete: Bool - - var body: some View { - 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) - } - - - func advance() { - withAnimation(.spring()) { - stepIndex += 1 - } - } - -} - -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.. 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 : 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 { - VStack { - Text(title) - .font(.title) - Spacer() - image - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 64) - Spacer() - 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) - } - } - -} - -extension SetupView { - - enum Constants { - static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")! - } - -} - -struct ShellConfigInstruction: Identifiable, Hashable { - - 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 diff --git a/Sources/Secretive/Views/Styles/ActionButtonStyle.swift b/Sources/Secretive/Views/Styles/ActionButtonStyle.swift new file mode 100644 index 0000000..74284a7 --- /dev/null +++ b/Sources/Secretive/Views/Styles/ActionButtonStyle.swift @@ -0,0 +1,94 @@ +import SwiftUI + +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 + + func body(content: Content) -> some View { + // Tinted glass prominent is really hard to read on 26.0. + if #available(macOS 26.0, *), colorScheme == .dark { + content.buttonStyle(.glassProminent) + .tint(.red) + .foregroundStyle(.white) + } else { + content.buttonStyle(.borderedProminent) + .tint(.red) + .foregroundStyle(.white) + } + } + +} + +extension View { + + func danger() -> some View { + modifier(DangerButtonModifier()) + } + +} diff --git a/Sources/Secretive/Views/Styles/BoxBackgroundStyle.swift b/Sources/Secretive/Views/Styles/BoxBackgroundStyle.swift new file mode 100644 index 0000000..8ffbf38 --- /dev/null +++ b/Sources/Secretive/Views/Styles/BoxBackgroundStyle.swift @@ -0,0 +1,32 @@ +import SwiftUI + +struct BoxBackgroundModifier: ViewModifier { + + let color: Color + + func body(content: Content) -> some View { + content + .background { + RoundedRectangle(cornerRadius: 5) + .fill(color.opacity(0.3)) + .stroke(color, lineWidth: 1) + } + } +} + +extension View { + + func boxBackground(color: Color) -> some View { + modifier(BoxBackgroundModifier(color: color)) + } + +} + +#Preview { + Text("Hello") + .boxBackground(color: .red) + .padding() + Text("Hello") + .boxBackground(color: .orange) + .padding() +} diff --git a/Sources/Secretive/Views/Styles/ErrorStyle.swift b/Sources/Secretive/Views/Styles/ErrorStyle.swift new file mode 100644 index 0000000..18917f1 --- /dev/null +++ b/Sources/Secretive/Views/Styles/ErrorStyle.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct ErrorStyleModifier: ViewModifier { + + func body(content: Content) -> some View { + content + .foregroundStyle(.red) + .font(.callout) + } + +} + +extension View { + + func errorStyle() -> some View { + modifier(ErrorStyleModifier()) + } + +} diff --git a/Sources/Secretive/Views/ToolbarButtonStyle.swift b/Sources/Secretive/Views/Styles/ToolbarButtonStyle.swift similarity index 100% rename from Sources/Secretive/Views/ToolbarButtonStyle.swift rename to Sources/Secretive/Views/Styles/ToolbarButtonStyle.swift diff --git a/Sources/Secretive/Views/Views/AgentStatusView.swift b/Sources/Secretive/Views/Views/AgentStatusView.swift new file mode 100644 index 0000000..50b50c9 --- /dev/null +++ b/Sources/Secretive/Views/Views/AgentStatusView.swift @@ -0,0 +1,153 @@ +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 + + 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: URL.socketPath, + action: .copy(URL.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)) +//} diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/Views/ContentView.swift similarity index 61% rename from Sources/Secretive/Views/ContentView.swift rename to Sources/Secretive/Views/Views/ContentView.swift index dfc6dff..54b0fe5 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/Views/ContentView.swift @@ -36,7 +36,7 @@ struct ContentView: View { toolbarItem(newItemView, id: "new") } .sheet(isPresented: $runningSetup) { - SetupView(visible: $runningSetup, setupComplete: $hasRunSetup) + SetupView(setupComplete: $hasRunSetup) } } @@ -56,7 +56,7 @@ extension ContentView { } var needsSetup: Bool { - (runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild + runningSetup || !hasRunSetup } /// 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 { setupNoticeView } else { - runningNoticeView + agentStatusToolbarView } } @@ -75,7 +75,7 @@ extension ContentView { if update.critical { return (.updateCriticalNoticeTitle, .red) } else { - if updater.testBuild { + if updater.currentVersion.isTestBuild { return (.updateTestNoticeTitle, .blue) } else { return (.updateNormalNoticeTitle, .orange) @@ -94,8 +94,23 @@ extension ContentView { .foregroundColor(.white) }) .buttonStyle(ToolbarButtonStyle(color: color)) - .popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in - UpdateDetailView(update: update) + .sheet(item: $selectedUpdate) { update in + VStack { + if updater.currentVersion.isTestBuild { + VStack { + if let description = updater.currentVersion.previewDescription { + Text(description) + } + Link(destination: URL(string: "https://github.com/maxgoedjen/secretive/actions/workflows/nightly.yml")!) { + Button(.updaterDownloadLatestNightlyButton) {} + .frame(maxWidth: .infinity) + .primaryButton() + } + } + .padding() + } + UpdateDetailView(update: update) + } } } } @@ -103,18 +118,17 @@ extension ContentView { @ViewBuilder var newItemView: some View { if storeList.modifiableStore?.isAvailable ?? false { - Button(action: { + Button(.appMenuNewSecretButton, systemImage: "plus") { showingCreation = true - }, label: { - Image(systemName: "plus") - }) + } + .menuButton() .sheet(isPresented: $showingCreation) { if let modifiable = storeList.modifiableStore { - CreateSecretView(store: modifiable, showing: $showingCreation) - .onDisappear { - guard let newest = modifiable.secrets.last else { return } - activeSecret = newest + CreateSecretView(store: modifiable) { created in + if let created { + activeSecret = created } + } } } } @@ -125,43 +139,44 @@ extension ContentView { Button(action: { runningSetup = true }, label: { - Group { - if hasRunSetup && !agentStatusChecker.running { - Text(.agentNotRunningNoticeTitle) - } else { - Text(.agentSetupNoticeTitle) - } + if !hasRunSetup { + Text(.agentSetupNoticeTitle) + .font(.headline) } - .font(.headline) - }) .buttonStyle(ToolbarButtonStyle(color: .orange)) } @ViewBuilder - var runningNoticeView: some View { + var agentStatusToolbarView: some View { Button(action: { showingAgentInfo = true }, label: { HStack { - Text(.agentRunningNoticeTitle) - .font(.headline) - .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) - Circle() - .frame(width: 10, height: 10) - .foregroundColor(Color.green) + if agentStatusChecker.running { + Text(.agentRunningNoticeTitle) + .font(.headline) + .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) + Circle() + .frame(width: 10, height: 10) + .foregroundColor(Color.green) + } else { + Text(.agentNotRunningNoticeTitle) + .font(.headline) + Circle() + .frame(width: 10, height: 10) + .foregroundColor(Color.red) + } } }) - .buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05))) + .buttonStyle( + 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) { - VStack { - Text(.agentRunningNoticeDetailTitle) - .font(.title) - .padding(5) - Text(.agentRunningNoticeDetailDescription) - .frame(width: 300) - } - .padding() + AgentStatusView() } } @@ -193,31 +208,22 @@ extension ContentView { } var attachmentAnchor: PopoverAttachmentAnchor { - // Ideally .point(.bottom), but broken on Sonoma (FB12726503) .rect(.bounds) } } -#if DEBUG - -struct ContentView_Previews: PreviewProvider { - - static var previews: some View { - Group { - // Empty on modifiable and nonmodifiable - ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) - .environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)])) - .environment(PreviewUpdater()) - - // 5 items on modifiable and nonmodifiable - ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) - .environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()])) - .environment(PreviewUpdater()) - } - - } -} - -#endif +//#Preview { +// // Empty on modifiable and nonmodifiable +// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) +// .environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)])) +// .environment(PreviewUpdater()) +//} +// +//#Preview { +// // 5 items on modifiable and nonmodifiable +// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true)) +// .environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()])) +// .environment(PreviewUpdater()) +//} diff --git a/Sources/Secretive/Views/CopyableView.swift b/Sources/Secretive/Views/Views/CopyableView.swift similarity index 77% rename from Sources/Secretive/Views/CopyableView.swift rename to Sources/Secretive/Views/Views/CopyableView.swift index d1be4be..5d5b431 100644 --- a/Sources/Secretive/Views/CopyableView.swift +++ b/Sources/Secretive/Views/Views/CopyableView.swift @@ -6,6 +6,7 @@ struct CopyableView: View { var title: LocalizedStringResource var image: Image var text: String + var showRevealInFinder = false @State private var interactionState: InteractionState = .normal @@ -21,9 +22,12 @@ struct CopyableView: View { .foregroundColor(primaryTextColor) Spacer() if interactionState != .normal { - hoverIcon - .bold() - .textCase(.uppercase) + HStack { + if showRevealInFinder { + revealInFinderButton + } + copyButton + } .foregroundColor(secondaryTextColor) .transition(.opacity) } @@ -72,19 +76,35 @@ struct CopyableView: View { } @ViewBuilder - var hoverIcon: some View { + var copyButton: some View { switch interactionState { case .hovering: - Image(systemName: "document.on.document") - .accessibilityLabel(String(localized: "copyable_click_to_copy_button")) + Button(.copyableClickToCopyButton, systemImage: "document.on.document") { + withAnimation { + // Button will eat the click, so we set interaction state manually. + interactionState = .clicking + } + copy() + } + .labelStyle(.iconOnly) + .buttonStyle(.borderless) case .clicking: Image(systemName: "checkmark.circle.fill") - .accessibilityLabel(String(localized: "copyable_copied")) + .accessibilityLabel(String(localized: .copyableCopied)) case .normal, .dragging: EmptyView() } } + var revealInFinderButton: some View { + Button(.revealInFinderButton, systemImage: "folder") { + let (processedPath, folder) = text.normalizedPathAndFolder + NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder) + } + .labelStyle(.iconOnly) + .buttonStyle(.borderless) + } + var primaryTextColor: Color { switch interactionState { case .normal, .hovering, .dragging: @@ -163,17 +183,12 @@ fileprivate struct BackgroundViewModifier: ViewModifier { } -#if DEBUG - -struct CopyableView_Previews: PreviewProvider { - static var previews: some View { - Group { - CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.") - .padding() - CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ") - .padding() - } - } +#Preview { + CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.") + .padding() } -#endif +#Preview { + CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ") + .padding() +} diff --git a/Sources/Secretive/Views/UpdateView.swift b/Sources/Secretive/Views/Views/UpdateView.swift similarity index 100% rename from Sources/Secretive/Views/UpdateView.swift rename to Sources/Secretive/Views/Views/UpdateView.swift