Compare commits

..

55 Commits

Author SHA1 Message Date
Max Goedjen
1f74bd814f Include tag name in release upload command (#716) 2025-09-14 23:28:56 +00:00
Max Goedjen
d9d93574f2 Fix repeat setup (#712)
* Fix repeat setup

* Ideal width
2025-09-14 23:15:32 +00:00
Max Goedjen
15e8ed1ec2 Fix issue where “mark as migrated” could fail (#715) 2025-09-14 23:11:54 +00:00
Max Goedjen
1df0c8e96b Switch to icon composer source. (#714) 2025-09-14 23:01:18 +00:00
Max Goedjen
8213a8b451 Ideal width (#713) 2025-09-14 22:41:05 +00:00
Max Goedjen
af77fd4a21 Fix release digest formatting (#711) 2025-09-14 22:17:53 +00:00
Max Goedjen
85d0cab0f5 Disable preview (#710) 2025-09-14 21:53:13 +00:00
Max Goedjen
e8cdcdfb7f Adding some size fixing (#709) 2025-09-14 21:48:22 +00:00
Max Goedjen
d7f8d5e56b Add descriptions for unavailable keys (#708)
* Describe unavailable key types

* Cleanup
2025-09-14 21:42:41 +00:00
Max Goedjen
3f247d628f About screen. (#707) 2025-09-14 21:39:20 +00:00
Vladimir
dae9cead4e Update Russian localization (#706) 2025-09-14 21:05:34 +00:00
Max Goedjen
fe9f8613fa Fix move app later text (#705) 2025-09-14 08:47:40 +00:00
Max Goedjen
5d5ae5bab4 Add app folder notice. (#704) 2025-09-14 08:43:00 +00:00
Max Goedjen
f76766a9d5 Updater UI (#703)
* Parse markdown oop

* Update UI.

* Tweaks.
2025-09-14 08:20:10 +00:00
Max Goedjen
b308b10716 UI tweaks. (#701) 2025-09-14 00:03:20 +00:00
Max Goedjen
0e1e6813a1 Readme updates (#700) 2025-09-13 22:50:05 +00:00
Max Goedjen
27bf7c29e4 Fix deployment version for xpc services (#699) 2025-09-13 11:56:37 -07:00
Max Goedjen
36b6c52979 Logging for xpc input parser (#698) 2025-09-13 16:45:51 +00:00
Max Goedjen
67ec4fee12 More UI tweaks and fixes (#697)
* Integrations to window

* Cleanup of presenting.

* Older name for copy

* For copyable view too
2025-09-13 08:16:23 +00:00
Max Goedjen
21fc834fd9 Fix incorrect deletion of tracked files in public key standin folder. (#696) 2025-09-13 01:52:17 +00:00
Max Goedjen
726d0580d0 Fix minor ui glitches on older macOS (#695)
* Fix padding on toolbar buttons

* Fix sizing on setup view.
2025-09-12 08:40:57 +00:00
Max Goedjen
4f608ebbc6 Clear out needs review status (#694) 2025-09-12 01:57:09 +00:00
Max Goedjen
6e7cf82618 Fix quotes (#693)
* Fix up strings (hopefully)

* Few more

* Fixed back sides
2025-09-12 01:46:20 +00:00
Max Goedjen
96ef91df0c Use macOS 26 runner (#691)
* Update macOS version in GitHub Actions workflow

* Update macOS version in release workflow

* Update macOS version for nightly workflow

* Fix runner OS for Swift in CodeQL workflow
2025-09-12 01:15:54 +00:00
Max Goedjen
aa46d8fa48 Pull in new localizations (#690) 2025-09-11 18:10:51 -07:00
Max Goedjen
cf7c6e9fbe Fix detail update on dismiss of edit screen. (#687) 2025-09-10 07:29:12 +00:00
Max Goedjen
7c7db56c1e Fix unavail modifiable empty store (#686) 2025-09-10 00:24:06 -07:00
Max Goedjen
cd12e4c828 Crowdin readme (#685) 2025-09-09 23:47:17 -07:00
Max Goedjen
a5b43ea046 Cleanup and string fixes (#684)
* Setup UI tweaks.

* Protection level string

* Toolbar cleanup

* More strings.
2025-09-09 23:00:26 -07:00
Max Goedjen
8c516e128a Enabling strict memory safety. (#683) 2025-09-09 20:41:29 -07:00
Max Goedjen
6854c05763 Remove deprecated warning setting from XPCWrappers (#682) 2025-09-09 06:28:38 +00:00
Max Goedjen
5467474d88 Switch to higher level XPC & enforce signing requirements (#681)
* Revert "Add launch constraints (#678)"

This reverts commit c5a610d786.

* .

* Cleanup.
2025-09-09 06:25:40 +00:00
Max Goedjen
5c2d039682 Update release.yml (#680) 2025-09-08 07:32:03 +00:00
Max Goedjen
20e64604d6 Replace headers with silgen (#679)
* Revert "Put back"

This reverts commit 37d791f787.

* Cleanup
2025-09-08 00:28:24 -07:00
Max Goedjen
c5a610d786 Add launch constraints (#678)
* XPC updater POC

* WIP

* obo

* WIP

* Working

* .

* .

* .

* Cleanup

* Cleanup

* Throw restrict

* Remove dead protocol.

* .

* Fix ECDSA test.

* Scripts

* WIP

* .

* Put back

* Put back
2025-09-08 07:20:24 +00:00
Max Goedjen
7d21e3983c Set teams (#677) 2025-09-08 06:50:33 +00:00
Max Goedjen
2c38aaed6f Move internet access policy to xpc (#676) 2025-09-07 06:28:58 +00:00
Max Goedjen
63b42bd9df Move downloader and socket input parsing to xpc services (#675)
* XPC updater POC

* WIP

* obo

* WIP

* Working

* .

* .

* .

* Cleanup

* Cleanup

* Throw restrict

* Remove dead protocol.

* .

* Fix ECDSA test.

* Scripts
2025-09-06 23:16:23 -07:00
Max Goedjen
cf5ae49ebc Remove unneeded protocosl (#674) 2025-09-06 19:53:10 +00:00
Max Goedjen
61705af42f Extend parsing to cover the full set of protocol messages (#673)
* WIP

* Cleanup

* WIP

* Strip out extensions

* Remove singature reader
2025-09-06 19:48:59 +00:00
Max Goedjen
558ae15b2d Missing secrets help (#671) 2025-09-04 08:10:50 +00:00
Max Goedjen
902d5c4a1e Nightly display version (#670)
* Link to nightly and parse version better

* Add nightly date.
2025-09-04 07:19:55 +00:00
Max Goedjen
e0c24917f2 Add codeql workflow (#655)
* Add codeql workflow

* Disable signing on codeql workflow

Updated build command to disable code signing.

* Archive

* Temporarily disable previews

* Try disabling hardening

* Release mode

* Previews

* Temporarily disable.

* Update xcodebuild command in codeql.yml

* Try swift build output

* Fix

* Revert "Fix"

This reverts commit 064172779a.

* Revert "Try swift build output"

This reverts commit a3aa982223.

* Reset most stuff back to main

* Cleanup workflows and limit a few jobs to read.

* .
2025-09-04 06:46:21 +00:00
Max Goedjen
3d5f0b45bd Add reveal in finder to copyable (#669) 2025-09-04 06:36:50 +00:00
Max Goedjen
74f4f1c0b1 Cleanup socketport construction (#668) 2025-09-04 06:14:55 +00:00
Max Goedjen
c8cf0db1c5 Remove some unused stirngs. (#664) 2025-09-04 05:03:48 +00:00
Max Goedjen
412687467b Digest wants 'sha256:' prefix that the upload step doesn't add for some reason (#667) 2025-09-04 05:01:44 +00:00
Max Goedjen
416a7d5f40 Fix attestation issue from double-zip file process in upload action (#666)
* Remove archives since we don’t strip symbols anyway.

* Attest upload result.
2025-09-03 21:38:22 -07:00
Max Goedjen
c4605fb60e Update nightly.yml (#665) 2025-09-03 08:49:20 +00:00
Max Goedjen
61eed5987c Add permissions to nightly (#663)
* Add permissions to nightly

* Add archives
2025-09-03 08:39:59 +00:00
Max Goedjen
cbef7c6181 Add manual trigger to nightly (#662) 2025-09-03 08:25:00 +00:00
Max Goedjen
63a09390b8 Temporarily disable previews (#661)
* Previews

* Temporarily disable.
2025-09-03 08:11:55 +00:00
Max Goedjen
a4e1ab9eb6 Move (#659) 2025-09-03 07:29:58 +00:00
Max Goedjen
147f4d9908 New setup (#657)
* WIP

* WIP

* WIP

* Tweaks.

* WIP

* WIP

* WIP

* WIP

* WIP

* Cleanup

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* REmove setup menu item

* WIP

* .

* .

* .

* Cleaup.
2025-09-03 07:20:24 +00:00
Max Goedjen
ddcb2a36ec Fixes to agent reloading. (#656) 2025-09-02 03:09:32 +00:00
107 changed files with 27774 additions and 7558 deletions

47
.github/workflows/codeql.yml vendored Normal file
View File

@@ -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-26') || '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}}"

View File

@@ -3,10 +3,15 @@ name: Nightly
on:
schedule:
- cron: "0 8 * * *"
workflow_dispatch:
jobs:
build:
# runs-on: macOS-latest
runs-on: macos-15
runs-on: macos-26
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 }}

View File

@@ -6,8 +6,9 @@ on:
- '*'
jobs:
test:
# runs-on: macOS-latest
runs-on: macos-15
permissions:
contents: read
runs-on: macos-26
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
@@ -23,14 +24,15 @@ jobs:
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
- name: Test
run: swift test --build-system swiftbuild --package-path Sources/Packages
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
# SPM doesn't seem to pick up on the tests currently?
# 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-26
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
@@ -53,42 +55,37 @@ jobs:
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/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
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
- 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: sha256:${{ 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
gh release upload $TAG_NAME Secretive.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

View File

@@ -3,14 +3,17 @@ name: Test
on: [push, pull_request]
jobs:
test:
# runs-on: macOS-latest
runs-on: macos-15
permissions:
contents: read
runs-on: macos-26
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
- name: Test Main Packages
run: swift test --build-system swiftbuild --package-path Sources/Packages
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
# SPM doesn't seem to pick up on the tests currently?
# run: swift test --build-system swiftbuild --package-path Sources/Packages
- name: Test SecretKit Packages
run: swift test --build-system swiftbuild

View File

@@ -2,36 +2,35 @@
If you speak another language, and would like to help translate Secretive to support that language, we'd love your help!
## Getting Started
## Crowdin
### Download Xcode
[Secretive uses Crowdin for localization](https://crowdin.com/project/secretive/). Open the link and select your language to translate!
Download the latest version of Xcode (at minimum, Xcode 15) from [Apple](http://developer.apple.com/download/applications/).
### Manual Translation
### Clone Secretive
Clone Secretive using [these instructions from GitHub](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository).
### Open Secretive
Open [Sources/Secretive.xcodeproj](Sources/Secretive.xcodeproj) in Xcode.
### Translate
Navigate to [Secretive/Localizable](Sources/Secretive/Localizable.xcstrings).
<img src="/.github/readme/localize_sidebar.png" alt="Screenshot of Xcode navigating to the Localizable file" width="300">
If your language already has an in-progress localization, select it from the list. If it isn't there, hit the "+" button and choose your language from the list.
<img src="/.github/readme/localize_add.png" alt="Screenshot of Xcode adding a new language" width="600">
Start translating! You'll see a list of english phrases, and a space to add a translation of your language.
### Create a Pull Request
Push your changes and open a pull request.
Crowdin is the easiest way to translate Secretive, but I'm happy to accept Pull Requests directly as well.
### Questions
Please open an issue if you have a question about translating the app. I'm more than happy to clarify any terms that are ambiguous or confusing. Thanks for contributing!
### Thank You
Thanks to all the folks who have contributed localizations so far!
- @mtardy for the French localization
- @GravityRyu for the Chinese localization
- @Saeger for the Portuguese (Brazil) localization
- @moritzsternemann for the German localization
- @RoboRich00A16 for the Italian localization
- @akx for the Finnish localization
- @mog422 for the Korean localization
- @niw for the Japanese localization
- @truita for the Catalan localization
- @Adimac93 for the Polish localization
- @alongotv for the Russian localization
A special thanks to [Crowdin](https://crowdin.com) for their [generous support of open source projects](https://crowdin.com/page/open-source-project-setup-request).

View File

@@ -57,7 +57,7 @@ let package = Package(
)
var localization: Resource {
.process("../../Localizable.xcstrings")
.process("../../Resources/Localizable.xcstrings")
}
var swiftSettings: [PackageDescription.SwiftSetting] {

View File

@@ -1,8 +1,7 @@
# Secretive [![Test](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) ![Release](https://github.com/maxgoedjen/secretive/workflows/Release/badge.svg)
Secretive is an app for storing and managing SSH keys in the Secure Enclave. It is inspired by the [sekey project](https://github.com/sekey/sekey), but rewritten in Swift with no external dependencies and with a handy native management app.
Secretive is an app for protecting and managing SSH keys with the Secure Enclave.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="/.github/readme/app-dark.png">
<img src="/.github/readme/app-light.png" alt="Screenshot of Secretive" width="600">
@@ -62,3 +61,11 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
## 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)
## Acknowledgements
### sekey
Secretive was inspired by the [sekey project](https://github.com/sekey/sekey).
### Localization
Secretive is localized to many languages by a generous team of volunteers. To learn more, see [LOCALIZING.md](LOCALIZING.md). Secretive's localization workflow is generously provided by [Crowdin](https://crowdin.com).

View File

@@ -1,2 +1,3 @@
CI_VERSION = GITHUB_CI_VERSION
CI_BUILD_NUMBER = GITHUB_BUILD_NUMBER
CI_BUILD_LINK = GITHUB_BUILD_URL

View File

@@ -13,12 +13,24 @@
},
"testTargets" : [
{
"enabled" : false,
"parallelizable" : true,
"target" : {
"containerPath" : "container:Secretive.xcodeproj",
"identifier" : "50617D9323FCE48E0099B055",
"name" : "SecretiveTests"
"containerPath" : "container:Packages",
"identifier" : "BriefTests",
"name" : "BriefTests"
}
},
{
"target" : {
"containerPath" : "container:Packages",
"identifier" : "SecretKitTests",
"name" : "SecretKitTests"
}
},
{
"target" : {
"containerPath" : "container:Packages",
"identifier" : "SecretAgentKitTests",
"name" : "SecretAgentKitTests"
}
}
],

File diff suppressed because it is too large Load Diff

View File

@@ -21,13 +21,13 @@ let package = Package(
targets: ["SmartCardSecretKit"]),
.library(
name: "SecretAgentKit",
targets: ["SecretAgentKit"]),
.library(
name: "SecretAgentKitHeaders",
targets: ["SecretAgentKitHeaders"]),
targets: ["SecretAgentKit", "XPCWrappers"]),
.library(
name: "Brief",
targets: ["Brief"]),
.library(
name: "XPCWrappers",
targets: ["XPCWrappers"]),
],
dependencies: [
],
@@ -57,20 +57,17 @@ let package = Package(
),
.target(
name: "SecretAgentKit",
dependencies: ["SecretKit", "SecretAgentKitHeaders"],
dependencies: ["SecretKit"],
resources: [localization],
swiftSettings: swiftSettings,
),
.systemLibrary(
name: "SecretAgentKitHeaders",
),
.testTarget(
name: "SecretAgentKitTests",
dependencies: ["SecretAgentKit"],
),
.target(
name: "Brief",
dependencies: [],
dependencies: ["XPCWrappers"],
resources: [localization],
swiftSettings: swiftSettings,
),
@@ -78,16 +75,21 @@ let package = Package(
name: "BriefTests",
dependencies: ["Brief"],
),
.target(
name: "XPCWrappers",
swiftSettings: swiftSettings,
),
]
)
var localization: Resource {
.process("../../Localizable.xcstrings")
.process("../../Resources/Localizable.xcstrings")
}
var swiftSettings: [PackageDescription.SwiftSetting] {
[
.swiftLanguageMode(.v6),
.treatAllWarnings(as: .error),
.strictMemorySafety()
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
import Foundation
import SwiftUI
/// A release is a representation of a downloadable update.
public struct Release: Codable, Sendable {
public struct Release: Codable, Sendable, Hashable {
/// The user-facing name of the release. Typically "Secretive 1.2.3"
public let name: String
@@ -15,6 +16,8 @@ public struct Release: Codable, Sendable {
/// A user-facing description of the contents of the update.
public let body: String
public let attributedBody: AttributedString
/// Initializes a Release.
/// - Parameters:
/// - name: The user-facing name of the release.
@@ -26,6 +29,56 @@ public struct Release: Codable, Sendable {
self.prerelease = prerelease
self.html_url = html_url
self.body = body
self.attributedBody = AttributedString(_markdown: body)
}
public init(_ release: GitHubRelease) {
self.name = release.name
self.prerelease = release.prerelease
self.html_url = release.html_url
self.body = release.body
self.attributedBody = AttributedString(_markdown: release.body)
}
}
public struct GitHubRelease: Codable, Sendable {
let name: String
let prerelease: Bool
let html_url: URL
let body: String
}
fileprivate extension AttributedString {
init(_markdown markdown: String) {
let split = markdown.split(whereSeparator: \.isNewline)
let lines = split
.compactMap {
try? AttributedString(markdown: String($0), options: .init(allowsExtendedAttributes: true, interpretedSyntax: .full))
}
.map { (string: AttributedString) in
guard case let .header(level) = string.runs.first?.presentationIntent?.components.first?.kind else { return string }
return AttributedString("\n") + string
.transformingAttributes(\.font) { font in
font.value = switch level {
case 2: .headline.bold()
case 3: .headline
default: .subheadline
}
}
.transformingAttributes(\.underlineStyle) { underline in
underline.value = switch level {
case 2: .single
default: .none
}
}
+ AttributedString("\n")
}
self = lines.reduce(into: AttributedString()) { partialResult, next in
partialResult.append(next)
partialResult.append(AttributedString("\n"))
}
}
}

View File

@@ -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
}
}

View File

@@ -1,5 +1,6 @@
import Foundation
import Observation
import XPCWrappers
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
@Observable public final class Updater: UpdaterProtocol, Sendable {
@@ -13,12 +14,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,28 +34,25 @@ import Observation
) {
self.osVersion = osVersion
self.currentVersion = currentVersion
testBuild = currentVersion == SemVer("0.0.0")
Task {
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
Task {
await checkForUpdates()
try await checkForUpdates()
}
}
Task {
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(Int(checkFrequency)))
await checkForUpdates()
try await checkForUpdates()
}
}
}
/// Manually trigger an update check.
public func checkForUpdates() async {
guard let (data, _) = try? await URLSession.shared.data(from: Constants.updateURL) else { return }
guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
await evaluate(releases: releases)
public func checkForUpdates() async throws {
let session = try await XPCTypedSession<[Release], Never>(serviceName: "com.maxgoedjen.Secretive.SecretiveUpdater")
await evaluate(releases: try await session.send())
session.complete()
}
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
/// - Parameter release: The release to ignore.
public func ignore(release: Release) async {
@@ -102,11 +99,3 @@ extension Updater {
}
}
extension Updater {
enum Constants {
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases")!
}
}

View File

@@ -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
}

View File

@@ -31,46 +31,29 @@ public final class Agent: Sendable {
extension Agent {
/// Handles an incoming request.
/// - Parameters:
/// - data: The data to handle.
/// - provenance: The origin of the request.
/// - Returns: A response data payload.
public func handle(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
logger.debug("Agent handling new data")
guard data.count > 4 else {
throw InvalidDataProvidedError()
}
let requestTypeInt = data[4]
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
return SSHAgent.ResponseType.agentFailure.data.lengthAndData
}
logger.debug("Agent handling request of type \(requestType.debugDescription)")
let subData = Data(data[5...])
let response = await handle(requestType: requestType, data: subData, provenance: provenance)
return response
}
private func handle(requestType: SSHAgent.RequestType, data: Data, provenance: SigningRequestProvenance) async -> Data {
public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data {
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
await reloadSecretsIfNeccessary()
var response = Data()
do {
switch requestType {
switch request {
case .requestIdentities:
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
response.append(SSHAgent.Response.agentIdentitiesAnswer.data)
response.append(await identities())
logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
case .signRequest:
response.append(SSHAgent.ResponseType.agentSignResponse.data)
response.append(try await sign(data: data, provenance: provenance))
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)")
case .signRequest(let context):
response.append(SSHAgent.Response.agentSignResponse.data)
response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance))
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
case .unknown(let value):
logger.error("Agent received unknown request of type \(value).")
default:
logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.")
throw UnhandledRequestError()
}
} catch {
response.removeAll()
response.append(SSHAgent.ResponseType.agentFailure.data)
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
response = SSHAgent.Response.agentFailure.data
logger.debug("Agent returned \(SSHAgent.Response.agentFailure.debugDescription)")
}
return response.lengthAndData
}
@@ -101,7 +84,7 @@ extension Agent {
}
logger.log("Agent enumerated \(count) identities")
var countBigEndian = UInt32(count).bigEndian
let countData = Data(bytes: &countBigEndian, count: UInt32.bitWidth/8)
let countData = unsafe Data(bytes: &countBigEndian, count: MemoryLayout<UInt32>.size)
return countData + keyData
}
@@ -110,27 +93,16 @@ extension Agent {
/// - data: The data to sign.
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
let reader = OpenSSHReader(data: data)
let payloadHash = reader.readNextChunk()
let hash: Data
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
if let certificatePublicKey = await certificateHandler.publicKeyHash(from: payloadHash) {
hash = certificatePublicKey
} else {
hash = payloadHash
}
guard let (secret, store) = await secret(matching: hash) else {
logger.debug("Agent did not have a key matching \(hash as NSData)")
func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data {
guard let (secret, store) = await secret(matching: keyBlob) else {
let keyBlobHex = keyBlob.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }.joined()
logger.debug("Agent did not have a key matching \(keyBlobHex)")
throw NoMatchingKeyError()
}
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
let dataToSign = reader.readNextChunk()
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance)
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
try await witness?.witness(accessTo: secret, from: store, by: provenance)
@@ -169,16 +141,16 @@ extension Agent {
extension Agent {
struct InvalidDataProvidedError: Error {}
struct NoMatchingKeyError: Error {}
struct UnhandledRequestError: Error {}
}
extension SSHAgent.ResponseType {
extension SSHAgent.Response {
var data: Data {
var raw = self.rawValue
return Data(bytes: &raw, count: UInt8.bitWidth/8)
return unsafe Data(bytes: &raw, count: MemoryLayout<UInt8>.size)
}
}

View File

@@ -1,32 +1,12 @@
import Foundation
/// Protocol abstraction of the reading aspects of FileHandle.
public protocol FileHandleReader: Sendable {
/// Gets data that is available for reading.
var availableData: Data { get }
/// A file descriptor of the handle.
var fileDescriptor: Int32 { get }
/// The process ID of the process coonnected to the other end of the FileHandle.
var pidOfConnectedProcess: Int32 { get }
}
/// Protocol abstraction of the writing aspects of FileHandle.
public protocol FileHandleWriter: Sendable {
/// Writes data to the handle.
func write(_ data: Data)
}
extension FileHandle: FileHandleReader, FileHandleWriter {
extension FileHandle {
public var pidOfConnectedProcess: Int32 {
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1)
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout<Int32>.size, alignment: 1)
var len = socklen_t(MemoryLayout<Int32>.size)
getsockopt(fileDescriptor, SOCK_STREAM, LOCAL_PEERPID, pidPointer, &len)
return pidPointer.load(as: Int32.self)
unsafe getsockopt(fileDescriptor, SOCK_STREAM, LOCAL_PEERPID, pidPointer, &len)
return unsafe pidPointer.load(as: Int32.self)
}
}

View File

@@ -1,5 +1,6 @@
import Foundation
import OSLog
import SecretKit
/// Manages storage and lookup for OpenSSH certificates.
public actor OpenSSHCertificateHandler: Sendable {
@@ -25,29 +26,6 @@ public actor OpenSSHCertificateHandler: Sendable {
}
}
/// Reconstructs a public key from a ``Data``, if that ``Data`` contains an OpenSSH certificate hash. Currently only ecdsa certificates are supported
/// - Parameter certBlock: The openssh certificate to extract the public key from
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
public func publicKeyHash(from hash: Data) -> Data? {
let reader = OpenSSHReader(data: hash)
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
switch certType {
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
_ = reader.readNextChunk() // nonce
let curveIdentifier = reader.readNextChunk()
let publicKey = reader.readNextChunk()
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
return openSSHIdentifier.lengthAndData +
curveIdentifier.lengthAndData +
publicKey.lengthAndData
default:
return nil
}
}
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
/// - Parameter secret: The secret to search for a certificate with
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.

View File

@@ -0,0 +1,47 @@
import Foundation
/// Reads OpenSSH protocol data.
final class OpenSSHReader {
var remaining: Data
/// Initialize the reader with an OpenSSH data payload.
/// - Parameter data: The data to read.
init(data: Data) {
remaining = Data(data)
}
/// Reads the next chunk of data from the playload.
/// - Returns: The next chunk of data.
func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data {
let littleEndianLength = try readNextBytes(as: UInt32.self)
let length = convertEndianness ? Int(littleEndianLength.bigEndian) : Int(littleEndianLength)
guard remaining.count >= length else { throw .beyondBounds }
let dataRange = 0..<length
let ret = Data(remaining[dataRange])
remaining.removeSubrange(dataRange)
return ret
}
func readNextBytes<T>(as: T.Type) throws(OpenSSHReaderError) -> T {
let size = MemoryLayout<T>.size
guard remaining.count >= size else { throw .beyondBounds }
let lengthRange = 0..<size
let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(lengthRange)
return unsafe lengthChunk.bytes.unsafeLoad(as: T.self)
}
func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
}
func readNextChunkAsSubReader(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> OpenSSHReader {
OpenSSHReader(data: try readNextChunk(convertEndianness: convertEndianness))
}
}
public enum OpenSSHReaderError: Error, Codable {
case beyondBounds
}

View File

@@ -0,0 +1,109 @@
import Foundation
import OSLog
import SecretKit
public protocol SSHAgentInputParserProtocol {
func parse(data: Data) async throws -> SSHAgent.Request
}
public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "InputParser")
public init() {
}
public func parse(data: Data) throws(AgentParsingError) -> SSHAgent.Request {
logger.debug("Parsing new data")
guard data.count > 4 else {
throw .invalidData
}
let specifiedLength = unsafe (data[0..<4].bytes.unsafeLoad(as: UInt32.self).bigEndian) + 4
let rawRequestInt = data[4]
let remainingDataRange = 5..<min(Int(specifiedLength), data.count)
lazy var body: Data = { Data(data[remainingDataRange]) }()
switch rawRequestInt {
case SSHAgent.Request.requestIdentities.protocolID:
return .requestIdentities
case SSHAgent.Request.signRequest(.empty).protocolID:
do {
return .signRequest(try signatureRequestContext(from: body))
} catch {
throw .openSSHReader(error)
}
case SSHAgent.Request.addIdentity.protocolID:
return .addIdentity
case SSHAgent.Request.removeIdentity.protocolID:
return .removeIdentity
case SSHAgent.Request.removeAllIdentities.protocolID:
return .removeAllIdentities
case SSHAgent.Request.addIDConstrained.protocolID:
return .addIDConstrained
case SSHAgent.Request.addSmartcardKey.protocolID:
return .addSmartcardKey
case SSHAgent.Request.removeSmartcardKey.protocolID:
return .removeSmartcardKey
case SSHAgent.Request.lock.protocolID:
return .lock
case SSHAgent.Request.unlock.protocolID:
return .unlock
case SSHAgent.Request.addSmartcardKeyConstrained.protocolID:
return .addSmartcardKeyConstrained
case SSHAgent.Request.protocolExtension.protocolID:
return .protocolExtension
default:
return .unknown(rawRequestInt)
}
}
}
extension SSHAgentInputParser {
func signatureRequestContext(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext {
let reader = OpenSSHReader(data: data)
let rawKeyBlob = try reader.readNextChunk()
let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob
let dataToSign = try reader.readNextChunk()
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: dataToSign)
}
func certificatePublicKeyBlob(from hash: Data) -> Data? {
let reader = OpenSSHReader(data: hash)
do {
let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self)
switch certType {
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
_ = try reader.readNextChunk() // nonce
let curveIdentifier = try reader.readNextChunk()
let publicKey = try reader.readNextChunk()
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
return openSSHIdentifier.lengthAndData +
curveIdentifier.lengthAndData +
publicKey.lengthAndData
default:
return nil
}
} catch {
return nil
}
}
}
extension SSHAgentInputParser {
public enum AgentParsingError: Error, Codable {
case unknownRequest
case unhandledRequest
case invalidData
case openSSHReader(OpenSSHReaderError)
}
}

View File

@@ -6,39 +6,92 @@ public enum SSHAgent {}
extension SSHAgent {
/// The type of the SSH Agent Request, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
public enum RequestType: UInt8, CustomDebugStringConvertible {
public enum Request: CustomDebugStringConvertible, Codable, Sendable {
case requestIdentities = 11
case signRequest = 13
case requestIdentities
case signRequest(SignatureRequestContext)
case addIdentity
case removeIdentity
case removeAllIdentities
case addIDConstrained
case addSmartcardKey
case removeSmartcardKey
case lock
case unlock
case addSmartcardKeyConstrained
case protocolExtension
case unknown(UInt8)
public var protocolID: UInt8 {
switch self {
case .requestIdentities: 11
case .signRequest: 13
case .addIdentity: 17
case .removeIdentity: 18
case .removeAllIdentities: 19
case .addIDConstrained: 25
case .addSmartcardKey: 20
case .removeSmartcardKey: 21
case .lock: 22
case .unlock: 23
case .addSmartcardKeyConstrained: 26
case .protocolExtension: 27
case .unknown(let value): value
}
}
public var debugDescription: String {
switch self {
case .requestIdentities:
return "RequestIdentities"
case .signRequest:
return "SignRequest"
}
case .requestIdentities: "SSH_AGENTC_REQUEST_IDENTITIES"
case .signRequest: "SSH_AGENTC_SIGN_REQUEST"
case .addIdentity: "SSH_AGENTC_ADD_IDENTITY"
case .removeIdentity: "SSH_AGENTC_REMOVE_IDENTITY"
case .removeAllIdentities: "SSH_AGENTC_REMOVE_ALL_IDENTITIES"
case .addIDConstrained: "SSH_AGENTC_ADD_ID_CONSTRAINED"
case .addSmartcardKey: "SSH_AGENTC_ADD_SMARTCARD_KEY"
case .removeSmartcardKey: "SSH_AGENTC_REMOVE_SMARTCARD_KEY"
case .lock: "SSH_AGENTC_LOCK"
case .unlock: "SSH_AGENTC_UNLOCK"
case .addSmartcardKeyConstrained: "SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED"
case .protocolExtension: "SSH_AGENTC_EXTENSION"
case .unknown: "UNKNOWN_MESSAGE"
}
}
public struct SignatureRequestContext: Sendable, Codable {
public let keyBlob: Data
public let dataToSign: Data
public init(keyBlob: Data, dataToSign: Data) {
self.keyBlob = keyBlob
self.dataToSign = dataToSign
}
public static var empty: SignatureRequestContext {
SignatureRequestContext(keyBlob: Data(), dataToSign: Data())
}
}
}
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
public enum ResponseType: UInt8, CustomDebugStringConvertible {
public enum Response: UInt8, CustomDebugStringConvertible {
case agentFailure = 5
case agentSuccess = 6
case agentIdentitiesAnswer = 12
case agentSignResponse = 14
case agentExtensionFailure = 28
case agentExtensionResponse = 29
public var debugDescription: String {
switch self {
case .agentFailure:
return "AgentFailure"
case .agentSuccess:
return "AgentSuccess"
case .agentIdentitiesAnswer:
return "AgentIdentitiesAnswer"
case .agentSignResponse:
return "AgentSignResponse"
case .agentFailure: "SSH_AGENT_FAILURE"
case .agentSuccess: "SSH_AGENT_SUCCESS"
case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER"
case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE"
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
}
}
}

View File

@@ -2,7 +2,6 @@ import Foundation
import AppKit
import Security
import SecretKit
import SecretAgentKitHeaders
/// An object responsible for generating ``SecretKit.SigningRequestProvenance`` objects.
struct SigningRequestTracer {
@@ -10,12 +9,11 @@ struct SigningRequestTracer {
extension SigningRequestTracer {
/// Generates a ``SecretKit.SigningRequestProvenance`` from a ``FileHandleReader``.
/// - Parameter fileHandleReader: The reader involved in processing the request.
/// Generates a ``SecretKit.SigningRequestProvenance`` from a ``FileHandle``.
/// - Parameter fileHandle: The reader involved in processing the request.
/// - Returns: A ``SecretKit.SigningRequestProvenance`` describing the origin of the request.
func provenance(from fileHandleReader: FileHandleReader) -> SigningRequestProvenance {
let firstInfo = process(from: fileHandleReader.pidOfConnectedProcess)
func provenance(from fileHandle: FileHandle) -> SigningRequestProvenance {
let firstInfo = process(from: fileHandle.pidOfConnectedProcess)
var provenance = SigningRequestProvenance(root: firstInfo)
while NSRunningApplication(processIdentifier: provenance.origin.pid) == nil && provenance.origin.parentPID != nil {
provenance.chain.append(process(from: provenance.origin.parentPID!))
@@ -27,11 +25,11 @@ extension SigningRequestTracer {
/// - Parameter pid: The process ID to look up.
/// - Returns: a `kinfo_proc` struct describing the process ID.
func pidAndNameInfo(from pid: Int32) -> kinfo_proc {
var len = MemoryLayout<kinfo_proc>.size
var len = unsafe MemoryLayout<kinfo_proc>.size
let infoPointer = UnsafeMutableRawPointer.allocate(byteCount: len, alignment: 1)
var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]
sysctl(&name, UInt32(name.count), infoPointer, &len, nil, 0)
return infoPointer.load(as: kinfo_proc.self)
unsafe sysctl(&name, UInt32(name.count), infoPointer, &len, nil, 0)
return unsafe infoPointer.load(as: kinfo_proc.self)
}
/// Generates a ``SecretKit.SigningRequestProvenance.Process`` from a provided process ID.
@@ -39,18 +37,18 @@ extension SigningRequestTracer {
/// - Returns: A ``SecretKit.SigningRequestProvenance.Process`` describing the process.
func process(from pid: Int32) -> SigningRequestProvenance.Process {
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
let ppid = pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
let procName = withUnsafeMutablePointer(to: &pidAndNameInfo.kp_proc.p_comm.0) { pointer in
String(cString: pointer)
let ppid = unsafe pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
let procName = unsafe withUnsafeMutablePointer(to: &pidAndNameInfo.kp_proc.p_comm.0) { pointer in
unsafe String(cString: pointer)
}
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
_ = proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
let path = String(cString: pathPointer)
_ = unsafe proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
let path = unsafe String(cString: pathPointer)
var secCode: Unmanaged<SecCode>!
let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks]
SecCodeCreateWithPID(pid, SecCSFlags(), &secCode)
let valid = SecCodeCheckValidity(secCode.takeRetainedValue(), flags, nil) == errSecSuccess
unsafe SecCodeCreateWithPID(pid, SecCSFlags(), &secCode)
let valid = unsafe SecCodeCheckValidity(secCode.takeRetainedValue(), flags, nil) == errSecSuccess
return SigningRequestProvenance.Process(pid: pid, processName: procName, appName: appName(for: pid), iconURL: iconURL(for: pid), path: path, validSignature: valid, parentPID: ppid)
}
@@ -81,3 +79,11 @@ extension SigningRequestTracer {
}
}
// from libproc.h
@_silgen_name("proc_pidpath")
@discardableResult func proc_pidpath(_ pid: Int32, _ buffer: UnsafeMutableRawPointer!, _ buffersize: UInt32) -> Int32
//// from SecTask.h
@_silgen_name("SecCodeCreateWithPID")
@discardableResult func SecCodeCreateWithPID(_: Int32, _: SecCSFlags, _: UnsafeMutablePointer<Unmanaged<SecCode>?>!) -> OSStatus

View File

@@ -133,22 +133,21 @@ private extension SocketPort {
convenience init(path: String) {
var addr = sockaddr_un()
let length = unsafe withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
unsafe path.withCString { cstring in
let len = unsafe strlen(cstring)
unsafe strncpy(pointer, cstring, len)
return len
}
}
// 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<sockaddr_un>.size - MemoryLayout.size(ofValue: addr.sun_path) + length)
var len: Int = 0
withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
path.withCString { cstring in
len = strlen(cstring)
strncpy(pointer, cstring, len)
}
}
addr.sun_len = UInt8(len+2)
var data: Data!
withUnsafePointer(to: &addr) { pointer in
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
}
let data = unsafe Data(bytes: &addr, count: MemoryLayout<sockaddr_un>.size)
self.init(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!
}

View File

@@ -1,19 +0,0 @@
#import <Foundation/Foundation.h>
#import <Security/Security.h>
// Forward declarations
// from libproc.h
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
// from SecTask.h
OSStatus SecCodeCreateWithPID(int32_t, SecCSFlags, SecCodeRef *);
//! Project version number for SecretAgentKit.
FOUNDATION_EXPORT double SecretAgentKitVersionNumber;
//! Project version string for SecretAgentKit.
FOUNDATION_EXPORT const unsigned char SecretAgentKitVersionString[];

View File

@@ -1,4 +0,0 @@
module SecretAgentKitHeaders [system] {
header "include/SecretAgentKit.h"
export *
}

View File

@@ -64,7 +64,7 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
private let _create: @Sendable (String, Attributes) async throws -> AnySecret
private let _delete: @Sendable (AnySecret) async throws -> Void
private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
private let _supportedKeyTypes: @Sendable () -> [KeyType]
private let _supportedKeyTypes: @Sendable () -> KeyAvailability
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { AnySecret(try await secretStore.create(name: $0, attributes: $1)) }
@@ -87,7 +87,7 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab
try await _update(secret, name, attributes)
}
public var supportedKeyTypes: [KeyType] {
public var supportedKeyTypes: KeyAvailability {
_supportedKeyTypes()
}

View File

@@ -36,12 +36,12 @@ public struct KeychainError: Error {
/// A signing-related error.
public struct SigningError: Error {
/// The underlying error reported by the API, if one was returned.
public let error: SecurityError?
public let error: CFError?
/// Initializes a SigningError with an optional SecurityError.
/// - Parameter statusCode: The SecurityError, if one is applicable.
public init(error: SecurityError?) {
self.error = error
self.error = unsafe error?.takeRetainedValue()
}
}

View File

@@ -7,7 +7,7 @@ extension Data {
package var lengthAndData: Data {
let rawLength = UInt32(count)
var endian = rawLength.bigEndian
return Data(bytes: &endian, count: UInt32.bitWidth/8) + self
return unsafe Data(bytes: &endian, count: MemoryLayout<UInt32>.size) + self
}
}

View File

@@ -97,7 +97,7 @@ extension OpenSSHPublicKeyWriter {
extension OpenSSHPublicKeyWriter {
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
// Keychain stores it as a thin ASN.1 wrapper with this format:
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e]

View File

@@ -1,28 +0,0 @@
import Foundation
/// Reads OpenSSH protocol data.
public final class OpenSSHReader {
var remaining: Data
/// Initialize the reader with an OpenSSH data payload.
/// - Parameter data: The data to read.
public init(data: Data) {
remaining = Data(data)
}
/// Reads the next chunk of data from the playload.
/// - Returns: The next chunk of data.
public func readNextChunk() -> Data {
let lengthRange = 0..<(UInt32.bitWidth/8)
let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(lengthRange)
let littleEndianLength = lengthChunk.bytes.unsafeLoad(as: UInt32.self)
let length = Int(littleEndianLength.bigEndian)
let dataRange = 0..<length
let ret = Data(remaining[dataRange])
remaining.removeSubrange(dataRange)
return ret
}
}

View File

@@ -19,14 +19,16 @@ public final class PublicKeyFileStoreController: Sendable {
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
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 validPaths = Set(secrets.map { publicKeyPath(for: $0) })
.union(Set(secrets.map { sshCertificatePath(for: $0) }))
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() }
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: directory, withIntermediateDirectories: false, attributes: nil)

View File

@@ -62,10 +62,37 @@ public protocol SecretStoreModifiable<SecretType>: SecretStore {
/// - attributes: The new attributes for the secret.
func update(secret: SecretType, name: String, attributes: Attributes) async throws
var supportedKeyTypes: [KeyType] { get }
var supportedKeyTypes: KeyAvailability { get }
}
public struct KeyAvailability: Sendable {
public let available: [KeyType]
public let unavailable: [UnavailableKeyType]
public init(available: [KeyType], unavailable: [UnavailableKeyType]) {
self.available = available
self.unavailable = unavailable
}
public struct UnavailableKeyType: Sendable {
public let keyType: KeyType
public let reason: Reason
public init(keyType: KeyType, reason: Reason) {
self.keyType = keyType
self.reason = reason
}
public enum Reason: Sendable {
case macOSUpdateRequired
}
}
}
extension NSNotification.Name {
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed secrets)

View File

@@ -27,10 +27,10 @@ extension SecureEnclave {
kSecReturnAttributes: true
])
var privateUntyped: CFTypeRef?
SecItemCopyMatching(privateAttributes, &privateUntyped)
unsafe 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
@@ -40,35 +40,39 @@ extension SecureEnclave {
}
let ref = key[kSecValueRef] as! SecKey
let attributes = SecKeyCopyAttributes(ref) as! [CFString: Any]
let tokenObjectID = attributes[Constants.tokenObjectID] as! Data
let tokenObjectID = unsafe attributes[Constants.tokenObjectID] as! Data
let accessControl = attributes[kSecAttrAccessControl] as! SecAccessControl
// Best guess.
let auth: AuthenticationRequirement = String(describing: accessControl)
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
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)
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)
migrated = true
markMigrated(secret: secret, oldID: id)
migratedAny = true
} catch {
logger.error("Failed to migrate \(name): \(error.localizedDescription).")
}
if migrated {
}
if migratedAny {
store.reloadSecrets()
}
}
public func markMigrated(secret: Secret, oldID: Data) throws {
public func markMigrated(secret: Secret, oldID: Data) {
let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id
kSecAttrApplicationLabel: oldID
])
let newID = oldID + Constants.migrationMagicNumber
@@ -78,7 +82,7 @@ extension SecureEnclave {
let status = SecItemUpdate(updateQuery, updatedAttributes)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
logger.warning("Failed to mark \(secret.name) as migrated: \(status).")
}
}

View File

@@ -21,7 +21,7 @@ extension SecureEnclave {
/// - duration: The duration of the authorization context, in seconds.
init(secret: Secret, context: LAContext, duration: TimeInterval) {
self.secret = secret
self.context = context
unsafe self.context = context
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
self.monotonicExpiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds)
}
@@ -56,11 +56,9 @@ extension SecureEnclave {
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
if let durationString = formatter.string(from: duration) {
let durationString = formatter.string(from: duration)!
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
} else {
newContext.localizedReason = String(localized: .authContextPersistForDurationUnknown(secretName: secret.name))
}
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
guard success else { return }
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)

View File

@@ -2,7 +2,7 @@ import Foundation
import Observation
import Security
import CryptoKit
@preconcurrency import LocalAuthentication
import LocalAuthentication
import SecretKit
import os
@@ -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()
}
@@ -40,7 +40,7 @@ extension SecureEnclave {
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
context = existing.context
context = unsafe existing.context
} else {
let newContext = LAContext()
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
@@ -57,7 +57,7 @@ extension SecureEnclave {
kSecReturnData: true,
])
var untyped: CFTypeRef?
let status = SecItemCopyMatching(queryAttributes, &untyped)
let status = unsafe SecItemCopyMatching(queryAttributes, &untyped)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
@@ -121,12 +121,12 @@ extension SecureEnclave {
fatalError()
}
let access =
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
unsafe SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
flags,
&accessError)
if let error = accessError {
throw error.takeRetainedValue() as Error
if let error = unsafe accessError {
throw unsafe error.takeRetainedValue() as Error
}
let dataRep: Data
let publicKey: Data
@@ -186,17 +186,22 @@ extension SecureEnclave {
await reloadSecrets()
}
public var supportedKeyTypes: [KeyType] {
if #available(macOS 26, *) {
[
.ecdsa256,
.mldsa65,
.mldsa87,
]
public let supportedKeyTypes: KeyAvailability = {
let macOS26Keys: [KeyType] = [.mldsa65, .mldsa87]
let isAtLeastMacOS26 = if #available(macOS 26, *) {
true
} else {
[.ecdsa256]
false
}
return KeyAvailability(
available: [
.ecdsa256,
] + (isAtLeastMacOS26 ? macOS26Keys : []),
unavailable: (isAtLeastMacOS26 ? [] : macOS26Keys).map {
KeyAvailability.UnavailableKeyType(keyType: $0, reason: .macOSUpdateRequired)
}
)
}()
}
}
@@ -214,7 +219,7 @@ extension SecureEnclave.Store {
kSecReturnAttributes: true
])
var untyped: CFTypeRef?
SecItemCopyMatching(queryAttributes, &untyped)
unsafe SecItemCopyMatching(queryAttributes, &untyped)
guard let typed = untyped as? [[CFString: Any]] else { return }
let wrapped: [SecureEnclave.Secret] = typed.compactMap {
do {

View File

@@ -1,7 +1,7 @@
import Foundation
import Observation
import Security
@preconcurrency import CryptoTokenKit
@unsafe @preconcurrency import CryptoTokenKit
import LocalAuthentication
import SecretKit
@@ -70,7 +70,7 @@ extension SmartCard {
kSecReturnRef: true
])
var untyped: CFTypeRef?
let status = SecItemCopyMatching(attributes, &untyped)
let status = unsafe SecItemCopyMatching(attributes, &untyped)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
@@ -80,8 +80,8 @@ extension SmartCard {
let key = untypedSafe as! SecKey
var signError: SecurityError?
guard let algorithm = signatureAlgorithm(for: secret) else { throw UnsupportKeyType() }
guard let signature = SecKeyCreateSignature(key, algorithm, data as CFData, &signError) else {
throw SigningError(error: signError)
guard let signature = unsafe SecKeyCreateSignature(key, algorithm, data as CFData, &signError) else {
throw unsafe SigningError(error: signError)
}
return signature as Data
}
@@ -152,7 +152,7 @@ extension SmartCard.Store {
kSecReturnAttributes: true
])
var untyped: CFTypeRef?
SecItemCopyMatching(attributes, &untyped)
unsafe SecItemCopyMatching(attributes, &untyped)
guard let typed = untyped as? [[CFString: Any]] else { return }
let wrapped: [SecretType] = typed.compactMap {
let name = $0[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)

View File

@@ -0,0 +1,14 @@
import Foundation
@objc protocol _XPCProtocol: Sendable {
func process(_ data: Data, with reply: @Sendable @escaping (Data?, Error?) -> Void)
}
public protocol XPCProtocol<Input, Output>: Sendable {
associatedtype Input: Codable
associatedtype Output: Codable
func process(_ data: Input) async throws -> Output
}

View File

@@ -0,0 +1,72 @@
import Foundation
public final class XPCServiceDelegate: NSObject, NSXPCListenerDelegate {
private let exportedObject: ErasedXPCProtocol
public init<XPCProtocolType: XPCProtocol>(exportedObject: XPCProtocolType) {
self.exportedObject = ErasedXPCProtocol(exportedObject)
}
public func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
newConnection.exportedInterface = NSXPCInterface(with: (any _XPCProtocol).self)
let exportedObject = exportedObject
newConnection.exportedObject = exportedObject
newConnection.setCodeSigningRequirement("anchor apple generic and certificate leaf[subject.OU] = Z72PRUAWF6")
newConnection.resume()
return true
}
}
@objc private final class ErasedXPCProtocol: NSObject, _XPCProtocol {
let _process: @Sendable (Data, @Sendable @escaping (Data?, (any Error)?) -> Void) -> Void
public init<XPCProtocolType: XPCProtocol>(_ exportedObject: XPCProtocolType) {
_process = { data, reply in
Task { [reply] in
do {
let decoded = try JSONDecoder().decode(XPCProtocolType.Input.self, from: data)
let result = try await exportedObject.process(decoded)
let encoded = try JSONEncoder().encode(result)
reply(encoded, nil)
} catch {
if let error = error as? Codable & Error {
reply(nil, NSError(error))
} else {
// Sending cast directly tries to serialize it and crashes XPCEncoder.
let cast = error as NSError
reply(nil, NSError(domain: cast.domain, code: cast.code, userInfo: [NSLocalizedDescriptionKey: error.localizedDescription]))
}
}
}
}
}
func process(_ data: Data, with reply: @Sendable @escaping (Data?, (any Error)?) -> Void) {
_process(data, reply)
}
}
extension NSError {
private enum Constants {
static let domain = "com.maxgoedjen.secretive.xpcwrappers"
static let code = -1
static let dataKey = "underlying"
}
@nonobjc convenience init<ErrorType: Codable & Error>(_ error: ErrorType) {
let encoded = try? JSONEncoder().encode(error)
self.init(domain: Constants.domain, code: Constants.code, userInfo: [Constants.dataKey: encoded as Any])
}
@nonobjc public func underlying<ErrorType: Codable & Error>(as errorType: ErrorType.Type) -> ErrorType? {
guard domain == Constants.domain && code == Constants.code, let data = userInfo[Constants.dataKey] as? Data else { return nil }
return try? JSONDecoder().decode(ErrorType.self, from: data)
}
}

View File

@@ -0,0 +1,53 @@
import Foundation
public struct XPCTypedSession<ResponseType: Codable & Sendable, ErrorType: Error & Codable>: ~Copyable {
private let connection: NSXPCConnection
private let proxy: _XPCProtocol
public init(serviceName: String, warmup: Bool = false) async throws {
let connection = NSXPCConnection(serviceName: serviceName)
connection.remoteObjectInterface = NSXPCInterface(with: (any _XPCProtocol).self)
connection.setCodeSigningRequirement("anchor apple generic and certificate leaf[subject.OU] = Z72PRUAWF6")
connection.resume()
guard let proxy = connection.remoteObjectProxy as? _XPCProtocol else { fatalError() }
self.connection = connection
self.proxy = proxy
if warmup {
_ = try? await send()
}
}
public func send(_ message: some Encodable = Data()) async throws -> ResponseType {
let encoded = try JSONEncoder().encode(message)
return try await withCheckedThrowingContinuation { continuation in
proxy.process(encoded) { data, error in
do {
if let error {
throw error
}
guard let data else {
throw NoDataError()
}
let decoded = try JSONDecoder().decode(ResponseType.self, from: data)
continuation.resume(returning: decoded)
} catch {
if let typed = (error as NSError).underlying(as: ErrorType.self) {
continuation.resume(throwing: typed)
} else {
continuation.resume(throwing: error)
}
}
}
}
}
public func complete() {
connection.invalidate()
}
public struct NoDataError: Error {}
}

View File

@@ -8,19 +8,22 @@ import CryptoKit
// MARK: Identity Listing
// let testProvenance = SigningRequestProvenance(root: .init(pid: 0, processName: "Test", appName: "Test", iconURL: nil, path: /, validSignature: true, parentPID: nil))
@Test func emptyStores() async throws {
let agent = Agent(storeList: SecretStoreList())
let response = try await agent.handle(data: Constants.Requests.requestIdentities, provenance: .test)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
let response = await agent.handle(request: request, provenance: .test)
#expect(response == Constants.Responses.requestIdentitiesEmpty)
}
@Test func identitiesList() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list)
let response = try await agent.handle(data: Constants.Requests.requestIdentities, provenance: .test)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
let response = await agent.handle(request: request, provenance: .test)
let actual = OpenSSHReader(data: response)
let expected = OpenSSHReader(data: Constants.Responses.requestIdentitiesMultiple)
print(actual, expected)
#expect(response == Constants.Responses.requestIdentitiesMultiple)
}
@@ -29,40 +32,42 @@ import CryptoKit
@Test func noMatchingIdentities() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list)
let response = try await agent.handle(data: Constants.Requests.requestSignatureWithNoneMatching, provenance: .test)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching)
let response = await agent.handle(request: request, provenance: .test)
#expect(response == Constants.Responses.requestFailure)
}
// @Test func ecdsaSignature() async throws {
// let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
// let requestReader = OpenSSHReader(data: Constants.Requests.requestSignature[5...])
// _ = requestReader.readNextChunk()
// let dataToSign = requestReader.readNextChunk()
// let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
// let agent = Agent(storeList: list)
// await agent.handle(reader: stubReader, writer: stubWriter)
// let outer = OpenSSHReader(data: stubWriter.data[5...])
// let payload = outer.readNextChunk()
// let inner = OpenSSHReader(data: payload)
// _ = inner.readNextChunk()
// let signedData = inner.readNextChunk()
// let rsData = OpenSSHReader(data: signedData)
// var r = rsData.readNextChunk()
// var s = rsData.readNextChunk()
// // This is fine IRL, but it freaks out CryptoKit
// if r[0] == 0 {
// r.removeFirst()
// }
// if s[0] == 0 {
// s.removeFirst()
// }
// var rs = r
// rs.append(s)
// let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
// // Correct signature
// #expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
// .isValidSignature(signature, for: dataToSign))
// }
@Test func ecdsaSignature() async throws {
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
guard case SSHAgent.Request.signRequest(let context) = request else { return }
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list)
let response = await agent.handle(request: request, provenance: .test)
let responseReader = OpenSSHReader(data: response)
let length = try responseReader.readNextBytes(as: UInt32.self).bigEndian
let type = try responseReader.readNextBytes(as: UInt8.self).bigEndian
#expect(length == response.count - MemoryLayout<UInt32>.size)
#expect(type == SSHAgent.Response.agentSignResponse.rawValue)
let outer = OpenSSHReader(data: responseReader.remaining)
let inner = try outer.readNextChunkAsSubReader()
_ = try inner.readNextChunk()
let rsData = try inner.readNextChunkAsSubReader()
var r = try rsData.readNextChunk()
var s = try rsData.readNextChunk()
// This is fine IRL, but it freaks out CryptoKit
if r[0] == 0 {
r.removeFirst()
}
if s[0] == 0 {
s.removeFirst()
}
var rs = r
rs.append(s)
let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
// Correct signature
#expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
.isValidSignature(signature, for: context.dataToSign))
}
// MARK: Witness protocol
@@ -72,7 +77,7 @@ import CryptoKit
return true
}, witness: { _, _ in })
let agent = Agent(storeList: list, witness: witness)
let response = try await agent.handle(data: Constants.Requests.requestSignature, provenance: .test)
let response = await agent.handle(request: .signRequest(.empty), provenance: .test)
#expect(response == Constants.Responses.requestFailure)
}
@@ -85,7 +90,8 @@ import CryptoKit
witnessed = true
})
let agent = Agent(storeList: list, witness: witness)
_ = try await agent.handle(data: Constants.Requests.requestSignature, provenance: .test)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
_ = await agent.handle(request: request, provenance: .test)
#expect(witnessed)
}
@@ -100,7 +106,8 @@ import CryptoKit
witnessTrace = trace
})
let agent = Agent(storeList: list, witness: witness)
_ = try await agent.handle(data: Constants.Requests.requestSignature, provenance: .test)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
_ = await agent.handle(request: request, provenance: .test)
#expect(witnessTrace == speakNowTrace)
#expect(witnessTrace == .test)
}
@@ -112,7 +119,8 @@ import CryptoKit
let store = await list.stores.first?.base as! Stub.Store
store.shouldThrow = true
let agent = Agent(storeList: list)
let response = try await agent.handle(data: Constants.Requests.requestSignature, provenance: .test)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
let response = await agent.handle(request: request, provenance: .test)
#expect(response == Constants.Responses.requestFailure)
}
@@ -120,7 +128,7 @@ import CryptoKit
@Test func unhandledAdd() async throws {
let agent = Agent(storeList: SecretStoreList())
let response = try await agent.handle(data: Constants.Requests.addIdentity, provenance: .test)
let response = await agent.handle(request: .addIdentity, provenance: .test)
#expect(response == Constants.Responses.requestFailure)
}
@@ -146,14 +154,13 @@ extension AgentTests {
enum Requests {
static let requestIdentities = Data(base64Encoded: "AAAAAQs=")!
static let addIdentity = Data(base64Encoded: "AAAAARE=")!
static let requestSignatureWithNoneMatching = Data(base64Encoded: "AAABhA0AAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAO8AAAAgbqmrqPUtJ8mmrtaSVexjMYyXWNqjHSnoto7zgv86xvcyAAAAA2dpdAAAAA5zc2gtY29ubmVjdGlvbgAAAAlwdWJsaWNrZXkBAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAAA=")!
static let requestSignature = Data(base64Encoded: "AAABRA0AAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08AAADPAAAAIBIFsbCZ4/dhBmLNGHm0GKj7EJ4N8k/jXRxlyg+LFIYzMgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAAA==")!
}
enum Responses {
static let requestIdentitiesEmpty = Data(base64Encoded: "AAAABQwAAAAA")!
static let requestIdentitiesMultiple = Data(base64Encoded: "AAABKwwAAAACAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0")!
static let requestIdentitiesMultiple = Data(base64Encoded: "AAABLwwAAAACAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAFWVjZHNhLTI1NkBleGFtcGxlLmNvbQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAGEEspLMDmreMJverQkqKC9zF9ZUasn5uSWkbRlz1jNTCtuyH1KKm+VImL6wdAj47SbzwM6lEEC24AdfrR64P9i/bnS2i83v/4wQVtcZn+Et13QGgWlZst8lxCPzTookaVwMAAAAFWVjZHNhLTM4NEBleGFtcGxlLmNvbQ==")!
static let requestFailure = Data(base64Encoded: "AAAAAQU=")!
}

View File

@@ -1,18 +1,18 @@
import Foundation
import Testing
@testable import SecretKit
@testable import SecretAgentKit
@testable import SecureEnclaveSecretKit
@testable import SmartCardSecretKit
@Suite struct OpenSSHReaderTests {
@Test func signatureRequest() {
@Test func signatureRequest() throws {
let reader = OpenSSHReader(data: Constants.signatureRequest)
let hash = reader.readNextChunk()
let hash = try reader.readNextChunk()
#expect(hash == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQ=="))
let dataToSign = reader.readNextChunk()
let dataToSign = try reader.readNextChunk()
#expect(dataToSign == Data(base64Encoded: "AAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QU="))
let empty = reader.readNextChunk()
let empty = try reader.readNextChunk()
#expect(empty.isEmpty)
}

View File

@@ -82,7 +82,7 @@ extension Stub {
let privateKey: Data
init(keySize: Int, publicKey: Data, privateKey: Data) {
self.attributes = Attributes(keyType: .init(algorithm: .ecdsa, size: keySize), authentication: .notRequired)
self.attributes = Attributes(keyType: .init(algorithm: .ecdsa, size: keySize), authentication: .notRequired, publicKeyAttribution: "ecdsa-\(keySize)@example.com")
self.publicKey = publicKey
self.privateKey = privateKey
}

View File

@@ -6,85 +6,67 @@ import SmartCardSecretKit
import SecretAgentKit
import Brief
import Observation
import SwiftUI
extension EnvironmentValues {
private static let _agentStatusChecker = AgentStatusChecker()
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
}
@main
struct SecretAgent: App {
class AppDelegate: NSObject, NSApplicationDelegate {
@SceneBuilder var body: some Scene {
MenuBarExtra("Test", systemImage: "lock") {
AgentStatusView()
.fixedSize()
@MainActor private let storeList: SecretStoreList = {
let list = SecretStoreList()
let cryptoKit = SecureEnclave.Store()
let migrator = SecureEnclave.CryptoKitMigrator()
try? migrator.migrate(to: cryptoKit)
list.add(store: cryptoKit)
list.add(store: SmartCard.Store())
return list
}()
private let updater = Updater(checkOnLaunch: true)
private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
private lazy var agent: Agent = {
Agent(storeList: storeList, witness: notifier)
}()
private lazy var socketController: SocketController = {
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
return SocketController(path: path)
}()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
func applicationDidFinishLaunching(_ aNotification: Notification) {
logger.debug("SecretAgent finished launching")
Task {
let inputParser = try await XPCAgentInputParser()
for await session in socketController.sessions {
Task {
do {
for await message in session.messages {
let request = try await inputParser.parse(data: message)
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
try await session.write(agentResponse)
}
} catch {
try session.close()
}
}
}
}
Task {
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
}
}
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
notifier.prompt()
_ = withObservationTracking {
updater.update
} onChange: { [updater, notifier] in
Task {
guard !updater.currentVersion.isTestBuild else { return }
await notifier.notify(update: updater.update!) { release in
await updater.ignore(release: release)
}
}
}
.menuBarExtraStyle(.window)
}
}
//@main
//class AppDelegate: NSObject, NSApplicationDelegate {
//
// @MainActor private let storeList: SecretStoreList = {
// let list = SecretStoreList()
// let cryptoKit = SecureEnclave.Store()
// let migrator = SecureEnclave.CryptoKitMigrator()
// try? migrator.migrate(to: cryptoKit)
// list.add(store: cryptoKit)
// list.add(store: SmartCard.Store())
// return list
// }()
// private let updater = Updater(checkOnLaunch: true)
// private let notifier = Notifier()
// private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
// private lazy var agent: Agent = {
// Agent(storeList: storeList, witness: notifier)
// }()
// private lazy var socketController: SocketController = {
// let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
// return SocketController(path: path)
// }()
// private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
//
// func applicationDidFinishLaunching(_ aNotification: Notification) {
// logger.debug("SecretAgent finished launching")
// Task {
// for await session in socketController.sessions {
// Task {
// do {
// for await message in session.messages {
// let agentResponse = try await agent.handle(data: message, provenance: session.provenance)
// try await session.write(agentResponse)
// }
// } catch {
// try session.close()
// }
// }
// }
// }
// Task {
// for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
// }
// }
// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
// notifier.prompt()
// _ = withObservationTracking {
// updater.update
// } onChange: { [updater, notifier] in
// Task {
// guard !updater.testBuild else { return }
// await notifier.notify(update: updater.update!) { release in
// await updater.ignore(release: release)
// }
// }
// }
// }
//
//}
//

View File

@@ -9,22 +9,7 @@
<key>Website</key>
<string>https://github.com/maxgoedjen/secretive</string>
<key>Connections</key>
<array>
<dict>
<key>IsIncoming</key>
<false/>
<key>Host</key>
<string>api.github.com</string>
<key>NetworkProtocol</key>
<string>TCP</string>
<key>Port</key>
<string>443</string>
<key>Purpose</key>
<string>Secretive checks GitHub for new versions and security updates.</string>
<key>DenyConsequences</key>
<string>If you deny these connections, you will not be notified about new versions and critical security updates.</string>
</dict>
</array>
<array/>
<key>Services</key>
<array/>
</dict>

View File

@@ -0,0 +1,29 @@
import Foundation
import SecretAgentKit
import Brief
import XPCWrappers
import OSLog
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "XPCAgentInputParser")
private let session: XPCTypedSession<SSHAgent.Request, SSHAgentInputParser.AgentParsingError>
public init() async throws {
logger.debug("Creating XPCAgentInputParser")
session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretAgentInputParser", warmup: true)
logger.debug("XPCAgentInputParser is warmed up.")
}
public func parse(data: Data) async throws -> SSHAgent.Request {
logger.debug("Parsing input")
defer { logger.debug("Parsed input") }
return try await session.send(data)
}
deinit {
session.complete()
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>XPCService</key>
<dict>
<key>ServiceType</key>
<string>Application</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,17 @@
import Foundation
import OSLog
import XPCWrappers
import SecretAgentKit
final class SecretAgentInputParser: NSObject, XPCProtocol {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.SecretAgentInputParser", category: "SecretAgentInputParser")
func process(_ data: Data) async throws -> SSHAgent.Request {
let parser = SSHAgentInputParser()
let result = try parser.parse(data: data)
logger.log("Parser parsed message as type \(result.debugDescription)")
return result
}
}

View File

@@ -0,0 +1,7 @@
import Foundation
import XPCWrappers
let delegate = XPCServiceDelegate(exportedObject: SecretAgentInputParser())
let listener = NSXPCListener.service()
listener.delegate = delegate
listener.resume()

View File

@@ -19,30 +19,39 @@
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; };
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; };
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.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 */; };
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
5064ADD32E669B1100B1382C /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
5064ADD42E669B2300B1382C /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; };
5064ADD52E669B3000B1382C /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; };
5064ADD62E669B5F00B1382C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
5064ADD72E669B7E00B1382C /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
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 */; };
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50692D282E6FDB8D0043C7BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692D242E6FDB8D0043C7BB /* main.swift */; };
50692D2D2E6FDC000043C7BB /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 50692D2C2E6FDC000043C7BB /* XPCWrappers */; };
50692D2F2E6FDC2B0043C7BB /* SecretiveUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692D2E2E6FDC290043C7BB /* SecretiveUpdater.swift */; };
50692D312E6FDC390043C7BB /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 50692D302E6FDC390043C7BB /* Brief */; };
50692E5B2E6FF9D20043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50692E682E6FF9E20043C7BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692E632E6FF9E20043C7BB /* main.swift */; };
50692E692E6FF9E20043C7BB /* SecretAgentInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692E642E6FF9E20043C7BB /* SecretAgentInputParser.swift */; };
50692E6C2E6FFA510043C7BB /* SecretAgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 50692E6B2E6FFA510043C7BB /* SecretAgentKit */; };
50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */; };
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; };
@@ -61,6 +70,10 @@
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 */; };
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; };
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; };
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
50E4C4C92E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -71,9 +84,68 @@
remoteGlobalIDString = 50A3B78924026B7500D209EA;
remoteInfo = SecretAgent;
};
501577D32E6BC5DD004A37D0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 501577BC2E6BC5B4004A37D0;
remoteInfo = ReleasesDownloader;
};
50692D1B2E6FDB880043C7BB /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 50692D112E6FDB880043C7BB;
remoteInfo = SecretiveUpdater;
};
50692E592E6FF9D20043C7BB /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 50692E4F2E6FF9D20043C7BB;
remoteInfo = SecretAgentInputParser;
};
50692E6E2E6FFA5F0043C7BB /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 50692D112E6FDB880043C7BB;
remoteInfo = SecretiveUpdater;
};
50692E712E6FFA6E0043C7BB /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 50692E4F2E6FF9D20043C7BB;
remoteInfo = SecretAgentInputParser;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
501577C92E6BC5B4004A37D0 /* Embed XPC Services */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
dstSubfolderSpec = 16;
files = (
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
50692E5B2E6FF9D20043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
);
name = "Embed XPC Services";
runOnlyForDeploymentPostprocessing = 0;
};
501577D22E6BC5D4004A37D0 /* Embed XPC Services */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
dstSubfolderSpec = 16;
files = (
50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
);
name = "Embed XPC Services";
runOnlyForDeploymentPostprocessing = 0;
};
50617DBF23FCE4AB0099B055 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -111,16 +183,21 @@
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
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 = "<group>"; };
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
50617D8623FCE48E0099B055 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = "<group>"; };
@@ -128,8 +205,16 @@
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
50692BA52E6D5CC90043C7BB /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveUpdater.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
50692D232E6FDB8D0043C7BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50692D242E6FDB8D0043C7BB /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
50692D2E2E6FDC290043C7BB /* SecretiveUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretiveUpdater.swift; sourceTree = "<group>"; };
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretAgentInputParser.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
50692E622E6FF9E20043C7BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50692E632E6FF9E20043C7BB /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
50692E642E6FF9E20043C7BB /* SecretAgentInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretAgentInputParser.swift; sourceTree = "<group>"; };
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
508A58A9241E06B40069DC07 /* PreviewUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewUpdater.swift; sourceTree = "<group>"; };
508A58AB241E121B0069DC07 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
@@ -153,6 +238,9 @@
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowBackgroundStyle.swift; sourceTree = "<group>"; };
50E4C4C22E7765DF00C73783 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
50E4C4C72E777E4200C73783 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -167,6 +255,23 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
50692D0F2E6FDB880043C7BB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
50692D2D2E6FDC000043C7BB /* XPCWrappers in Frameworks */,
50692D312E6FDC390043C7BB /* Brief in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
50692E4D2E6FF9D20043C7BB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
50692E6C2E6FFA510043C7BB /* SecretAgentKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
50A3B78724026B7500D209EA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -190,6 +295,58 @@
path = Helpers;
sourceTree = "<group>";
};
504788ED2E681EB200B4556F /* Modifiers */ = {
isa = PBXGroup;
children = (
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */,
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
);
path = Modifiers;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
504788F02E681F0100B4556F /* Views */ = {
isa = PBXGroup;
children = (
50E4C4C22E7765DF00C73783 /* AboutView.swift */,
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
);
path = Views;
sourceTree = "<group>";
};
50617D7623FCE48D0099B055 = {
isa = PBXGroup;
children = (
@@ -197,6 +354,8 @@
50617D8123FCE48E0099B055 /* Secretive */,
50A3B78B24026B7500D209EA /* SecretAgent */,
508A58AF241E144C0069DC07 /* Config */,
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */,
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */,
50617D8023FCE48E0099B055 /* Products */,
5099A08B240243730062B6F2 /* Frameworks */,
);
@@ -207,6 +366,8 @@
children = (
50617D7F23FCE48E0099B055 /* Secretive.app */,
50A3B78A24026B7500D209EA /* SecretAgent.app */,
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */,
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */,
);
name = Products;
sourceTree = "<group>";
@@ -218,11 +379,10 @@
508A58B0241ED1C40069DC07 /* Views */,
508A58B1241ED1EA0069DC07 /* Controllers */,
50033AC427813F1C00253856 /* Helpers */,
50617D8623FCE48E0099B055 /* Assets.xcassets */,
50617D8E23FCE48E0099B055 /* Info.plist */,
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
506772C62424784600034DED /* Credits.rtf */,
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
50617D8823FCE48E0099B055 /* Preview Content */,
);
@@ -240,6 +400,27 @@
path = "Preview Content";
sourceTree = "<group>";
};
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */ = {
isa = PBXGroup;
children = (
50692D232E6FDB8D0043C7BB /* Info.plist */,
50692BA52E6D5CC90043C7BB /* InternetAccessPolicy.plist */,
50692D242E6FDB8D0043C7BB /* main.swift */,
50692D2E2E6FDC290043C7BB /* SecretiveUpdater.swift */,
);
path = SecretiveUpdater;
sourceTree = "<group>";
};
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */ = {
isa = PBXGroup;
children = (
50692E622E6FF9E20043C7BB /* Info.plist */,
50692E632E6FF9E20043C7BB /* main.swift */,
50692E642E6FF9E20043C7BB /* SecretAgentInputParser.swift */,
);
path = SecretAgentInputParser;
sourceTree = "<group>";
};
508A58AF241E144C0069DC07 /* Config */ = {
isa = PBXGroup;
children = (
@@ -252,24 +433,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 */,
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
504788EF2E681ED700B4556F /* Configuration */,
504788EE2E681EC300B4556F /* Secrets */,
504788ED2E681EB200B4556F /* Modifiers */,
504788F02E681F0100B4556F /* Views */,
);
path = Views;
sourceTree = "<group>";
@@ -277,6 +444,7 @@
508A58B1241ED1EA0069DC07 /* Controllers */ = {
isa = PBXGroup;
children = (
504788EB2E680DC400B4556F /* URLs.swift */,
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
@@ -297,6 +465,7 @@
children = (
50020BAF24064869003D4025 /* AppDelegate.swift */,
5018F54E24064786002EB505 /* Notifier.swift */,
501578122E6C0479004A37D0 /* XPCInputParser.swift */,
50A3B79524026B7600D209EA /* Main.storyboard */,
50A3B79824026B7600D209EA /* Info.plist */,
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
@@ -326,11 +495,14 @@
50617D7D23FCE48D0099B055 /* Resources */,
50617DBF23FCE4AB0099B055 /* Embed Frameworks */,
50C385AF240E438B00AF2719 /* CopyFiles */,
501577C92E6BC5B4004A37D0 /* Embed XPC Services */,
);
buildRules = (
);
dependencies = (
50142167278126B500BBAA70 /* PBXTargetDependency */,
50692D1C2E6FDB880043C7BB /* PBXTargetDependency */,
50692E5A2E6FF9D20043C7BB /* PBXTargetDependency */,
);
name = Secretive;
packageProductDependencies = (
@@ -343,6 +515,47 @@
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
productType = "com.apple.product-type.application";
};
50692D112E6FDB880043C7BB /* SecretiveUpdater */ = {
isa = PBXNativeTarget;
buildConfigurationList = 50692D1F2E6FDB880043C7BB /* Build configuration list for PBXNativeTarget "SecretiveUpdater" */;
buildPhases = (
50692D0E2E6FDB880043C7BB /* Sources */,
50692D0F2E6FDB880043C7BB /* Frameworks */,
50692D102E6FDB880043C7BB /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SecretiveUpdater;
packageProductDependencies = (
50692D2C2E6FDC000043C7BB /* XPCWrappers */,
50692D302E6FDC390043C7BB /* Brief */,
);
productName = SecretiveUpdater;
productReference = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */;
productType = "com.apple.product-type.xpc-service";
};
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */ = {
isa = PBXNativeTarget;
buildConfigurationList = 50692E5D2E6FF9D20043C7BB /* Build configuration list for PBXNativeTarget "SecretAgentInputParser" */;
buildPhases = (
50692E4C2E6FF9D20043C7BB /* Sources */,
50692E4D2E6FF9D20043C7BB /* Frameworks */,
50692E4E2E6FF9D20043C7BB /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SecretAgentInputParser;
packageProductDependencies = (
50692E6B2E6FFA510043C7BB /* SecretAgentKit */,
);
productName = SecretAgentInputParser;
productReference = 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */;
productType = "com.apple.product-type.xpc-service";
};
50A3B78924026B7500D209EA /* SecretAgent */ = {
isa = PBXNativeTarget;
buildConfigurationList = 50A3B79A24026B7600D209EA /* Build configuration list for PBXNativeTarget "SecretAgent" */;
@@ -351,10 +564,14 @@
50A3B78724026B7500D209EA /* Frameworks */,
50A3B78824026B7500D209EA /* Resources */,
50A5C18E240E4B4B00E2996C /* Embed Frameworks */,
501577D22E6BC5D4004A37D0 /* Embed XPC Services */,
);
buildRules = (
);
dependencies = (
501577D42E6BC5DD004A37D0 /* PBXTargetDependency */,
50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */,
50692E722E6FFA6E0043C7BB /* PBXTargetDependency */,
);
name = SecretAgent;
packageProductDependencies = (
@@ -375,13 +592,19 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1220;
LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 2600;
ORGANIZATIONNAME = "Max Goedjen";
TargetAttributes = {
50617D7E23FCE48D0099B055 = {
CreatedOnToolsVersion = 11.3;
};
50692D112E6FDB880043C7BB = {
CreatedOnToolsVersion = 26.0;
};
50692E4F2E6FF9D20043C7BB = {
CreatedOnToolsVersion = 26.0;
};
50A3B78924026B7500D209EA = {
CreatedOnToolsVersion = 11.4;
};
@@ -411,6 +634,8 @@
targets = (
50617D7E23FCE48D0099B055 /* Secretive */,
50A3B78924026B7500D209EA /* SecretAgent */,
50692D112E6FDB880043C7BB /* SecretiveUpdater */,
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
);
};
/* End PBXProject section */
@@ -421,13 +646,26 @@
buildActionMask = 2147483647;
files = (
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */,
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */,
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */,
506772C72424784600034DED /* Credits.rtf in Resources */,
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
50692D102E6FDB880043C7BB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
50692E4E2E6FF9D20043C7BB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
50A3B78824026B7500D209EA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -435,8 +673,8 @@
50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */,
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
50E4C4C92E777E4200C73783 /* AppIcon.icon in Resources */,
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -447,12 +685,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */,
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.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 */,
@@ -470,6 +714,7 @@
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 */,
@@ -477,17 +722,31 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
50692D0E2E6FDB880043C7BB /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
50692D2F2E6FDC2B0043C7BB /* SecretiveUpdater.swift in Sources */,
50692D282E6FDB8D0043C7BB /* main.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
50692E4C2E6FF9D20043C7BB /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
50692E682E6FF9E20043C7BB /* main.swift in Sources */,
50692E692E6FF9E20043C7BB /* SecretAgentInputParser.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
50A3B78624026B7500D209EA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5064ADD32E669B1100B1382C /* AgentStatusView.swift in Sources */,
5064ADD72E669B7E00B1382C /* ConfigurationItemView.swift in Sources */,
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
5064ADD52E669B3000B1382C /* BundleIDs.swift in Sources */,
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
5064ADD62E669B5F00B1382C /* LaunchAgentController.swift in Sources */,
5064ADD42E669B2300B1382C /* AgentStatusChecker.swift in Sources */,
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -499,6 +758,30 @@
target = 50A3B78924026B7500D209EA /* SecretAgent */;
targetProxy = 50142166278126B500BBAA70 /* PBXContainerItemProxy */;
};
501577D42E6BC5DD004A37D0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
targetProxy = 501577D32E6BC5DD004A37D0 /* PBXContainerItemProxy */;
};
50692D1C2E6FDB880043C7BB /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 50692D112E6FDB880043C7BB /* SecretiveUpdater */;
targetProxy = 50692D1B2E6FDB880043C7BB /* PBXContainerItemProxy */;
};
50692E5A2E6FF9D20043C7BB /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */;
targetProxy = 50692E592E6FF9D20043C7BB /* PBXContainerItemProxy */;
};
50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 50692D112E6FDB880043C7BB /* SecretiveUpdater */;
targetProxy = 50692E6E2E6FFA5F0043C7BB /* PBXContainerItemProxy */;
};
50692E722E6FFA6E0043C7BB /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */;
targetProxy = 50692E712E6FFA6E0043C7BB /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -583,6 +866,8 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_MEMORY_SAFETY = YES;
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES;
SWIFT_VERSION = 6.0;
};
name = Debug;
@@ -650,6 +935,8 @@
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_MEMORY_SAFETY = YES;
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES;
SWIFT_VERSION = 6.0;
};
name = Release;
@@ -670,7 +957,7 @@
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
@@ -710,7 +997,7 @@
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
@@ -734,6 +1021,225 @@
};
name = Release;
};
50692D202E6FDB880043C7BB /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = 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;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretiveUpdater/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
50692D212E6FDB880043C7BB /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = 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;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretiveUpdater/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Test;
};
50692D222E6FDB880043C7BB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = 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;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretiveUpdater/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretiveUpdater;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretiveUpdater;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
50692E5E2E6FF9D20043C7BB /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretAgentInputParser/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
50692E5F2E6FF9D20043C7BB /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretAgentInputParser/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Test;
};
50692E602E6FF9D20043C7BB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretAgentInputParser/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretAgentInputParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgentInputParser;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
508A5914241EF1A00069DC07 /* Test */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 508A58AB241E121B0069DC07 /* Config.xcconfig */;
@@ -804,6 +1310,8 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_MEMORY_SAFETY = YES;
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES;
SWIFT_VERSION = 6.0;
};
name = Test;
@@ -822,7 +1330,7 @@
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
@@ -856,7 +1364,7 @@
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
@@ -891,7 +1399,7 @@
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
@@ -927,7 +1435,7 @@
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
@@ -973,6 +1481,26 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
50692D1F2E6FDB880043C7BB /* Build configuration list for PBXNativeTarget "SecretiveUpdater" */ = {
isa = XCConfigurationList;
buildConfigurations = (
50692D202E6FDB880043C7BB /* Debug */,
50692D212E6FDB880043C7BB /* Test */,
50692D222E6FDB880043C7BB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
50692E5D2E6FF9D20043C7BB /* Build configuration list for PBXNativeTarget "SecretAgentInputParser" */ = {
isa = XCConfigurationList;
buildConfigurations = (
50692E5E2E6FF9D20043C7BB /* Debug */,
50692E5F2E6FF9D20043C7BB /* Test */,
50692E602E6FF9D20043C7BB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
50A3B79A24026B7600D209EA /* Build configuration list for PBXNativeTarget "SecretAgent" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -1022,6 +1550,18 @@
isa = XCSwiftPackageProductDependency;
productName = Brief;
};
50692D2C2E6FDC000043C7BB /* XPCWrappers */ = {
isa = XCSwiftPackageProductDependency;
productName = XPCWrappers;
};
50692D302E6FDC390043C7BB /* Brief */ = {
isa = XCSwiftPackageProductDependency;
productName = Brief;
};
50692E6B2E6FFA510043C7BB /* SecretAgentKit */ = {
isa = XCSwiftPackageProductDependency;
productName = SecretAgentKit;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 50617D7723FCE48D0099B055 /* Project object */;

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Config/Secretive.xctestplan">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -4,6 +4,110 @@ import SecureEnclaveSecretKit
import SmartCardSecretKit
import Brief
@main
struct Secretive: App {
@Environment(\.agentStatusChecker) var agentStatusChecker
@Environment(\.justUpdatedChecker) var justUpdatedChecker
@SceneBuilder var body: some Scene {
WindowGroup {
ContentView()
.environment(EnvironmentValues._secretStoreList)
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
guard hasRunSetup else { return }
agentStatusChecker.check()
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()
}
}
}
.commands {
AppCommands()
}
WindowGroup(id: String(describing: IntegrationsView.self)) {
IntegrationsView()
}
.windowResizability(.contentMinSize)
WindowGroup(id: String(describing: AboutView.self)) {
AboutView()
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
}
}
extension Secretive {
struct AppCommands: Commands {
@Environment(\.openWindow) var openWindow
@Environment(\.openURL) var openURL
@FocusedValue(\.showCreateSecret) var showCreateSecret
var body: some Commands {
CommandGroup(replacing: .appInfo) {
Button(.aboutMenuBarTitle, systemImage: "info.circle") {
openWindow(id: String(describing: AboutView.self))
}
}
CommandGroup(before: CommandGroupPlacement.appSettings) {
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
openWindow(id: String(describing: IntegrationsView.self))
}
}
CommandGroup(after: CommandGroupPlacement.newItem) {
Button(.appMenuNewSecretButton, systemImage: "plus") {
showCreateSecret?()
}
.keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift]))
.disabled(showCreateSecret?.isEnabled == false)
}
CommandGroup(replacing: .help) {
Button(.appMenuHelpButton) {
openURL(Constants.helpURL)
}
}
SidebarCommands()
}
}
}
extension Secretive {
private func reinstallAgent() {
Task {
_ = await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check()
if !agentStatusChecker.running {
forceLaunchAgent()
}
}
}
private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly.
Task {
_ = await LaunchAgentController().forceLaunch()
agentStatusChecker.check()
}
}
}
private enum Constants {
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
}
extension EnvironmentValues {
// This is injected through .environment modifier below instead of @Entry for performance reasons (basially, restrictions around init/mainactor causing delay in loading secrets/"empty screen" blip).
@@ -25,94 +129,30 @@ 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
}
}
@main
struct Secretive: App {
extension FocusedValues {
@Entry var showCreateSecret: OpenSheet?
}
private let justUpdatedChecker = JustUpdatedChecker()
@Environment(\.agentStatusChecker) var agentStatusChecker
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false
@State private var showingIntegrations = false
@State private var showingCreation = false
final class OpenSheet {
@SceneBuilder var body: some Scene {
WindowGroup {
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
.environment(EnvironmentValues._secretStoreList)
.onAppear {
if !hasRunSetup {
showingSetup = true
}
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
guard hasRunSetup else { return }
agentStatusChecker.check()
if agentStatusChecker.running && justUpdatedChecker.justUpdated {
// 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
}
.keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift]))
}
CommandGroup(replacing: .help) {
Button(.appMenuHelpButton) {
NSWorkspace.shared.open(Constants.helpURL)
}
}
SidebarCommands()
let closure: () -> Void
let isEnabled: Bool
init(isEnabled: Bool = true, closure: @escaping () -> Void) {
self.isEnabled = isEnabled
self.closure = closure
}
func callAsFunction() {
closure()
}
}
extension Secretive {
private func reinstallAgent() {
justUpdatedChecker.check()
Task {
_ = await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check()
if !agentStatusChecker.running {
forceLaunchAgent()
}
}
}
private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly.
Task {
_ = await LaunchAgentController().forceLaunch()
agentStatusChecker.check()
}
}
}
private enum Constants {
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
}

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,68 +0,0 @@
{
"images" : [
{
"filename" : "Icon-macOS-ClearDark-16x16@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "Icon-macOS-ClearDark-16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "Icon-macOS-ClearDark-32x32@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "Icon-macOS-ClearDark-32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "Icon-macOS-ClearDark-128x128@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "Icon-macOS-ClearDark-128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "Icon-macOS-ClearDark-256x256@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "Icon-macOS-ClearDark-256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "Icon-macOS-ClearDark-512x512@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "Icon-macOS-ClearDark-1024x1024@1x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -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() {
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"
}
}

View File

@@ -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)
}
}

View File

@@ -1,36 +0,0 @@
{\rtf1\ansi\ansicpg1252\cocoartf2580
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6119\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive"}}{\fldrslt
\f0\fs24 \cf0 GitHub Repository}}
\f0\fs24 \
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
\cf0 \
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
\
Special Thanks To:\
\
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive/graphs/contributors"}}{\fldrslt Contributors}}:\
{\field{\*\fldinst{HYPERLINK "https://github.com/0xflotus"}}{\fldrslt 0xflotus}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/aaron-trout"}}{\fldrslt Aaron Trout}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/EppO"}}{\fldrslt \cf0 Florent Monbillard}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/vladimyr"}}{\fldrslt Dario Vladovi\uc0\u263 }}\
{\field{\*\fldinst{HYPERLINK "https://github.com/lavalleeale"}}{\fldrslt Alex Lavallee}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/joshheyse"}}{\fldrslt Josh}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/diesal11"}}{\fldrslt Dylan Lundy}}\
\
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
\cf0 Testers:\
{\field{\*\fldinst{HYPERLINK "https://github.com/bdash"}}{\fldrslt Mark Rowe}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/danielctull"}}{\fldrslt Daniel Tull}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/esttorhe"}}{\fldrslt Esteban Torres}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/joeblau"}}{\fldrslt Joe Blau}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/marksands"}}{\fldrslt Mark Sands}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/mergesort"}}{\fldrslt Joe Fabisevich}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/phillco"}}{\fldrslt Phil Cohen}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/zackdotcomputer"}}{\fldrslt Zack Sheppard}}}

View File

@@ -24,6 +24,8 @@
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
<key>GitHubBuildLog</key>
<string>$(CI_BUILD_LINK)</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>

View File

@@ -9,22 +9,7 @@
<key>Website</key>
<string>https://github.com/maxgoedjen/secretive</string>
<key>Connections</key>
<array>
<dict>
<key>IsIncoming</key>
<false/>
<key>Host</key>
<string>api.github.com</string>
<key>NetworkProtocol</key>
<string>TCP</string>
<key>Port</key>
<string>443</string>
<key>Purpose</key>
<string>Secretive checks GitHub for new versions and security updates.</string>
<key>DenyConsequences</key>
<string>If you deny these connections, you will not be notified about new versions and critical security updates.</string>
</dict>
</array>
<array/>
<key>Services</key>
<array/>
</dict>

View File

@@ -60,16 +60,17 @@ extension Preview {
let id = UUID()
var name: String { "Modifiable Preview Store" }
let secrets: [Secret]
var supportedKeyTypes: [KeyType] {
if #available(macOS 26, *) {
[
var supportedKeyTypes: KeyAvailability {
return KeyAvailability(
available: [
.ecdsa256,
.mldsa65,
.mldsa87,
.mldsa87
],
unavailable: [
.init(keyType: .ecdsa384, reason: .macOSUpdateRequired)
]
} else {
[.ecdsa256]
}
)
}
init(secrets: [Secret]) {

View File

@@ -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 {

View File

@@ -32,7 +32,7 @@ struct ConfigurationItemView<Content: View>: View {
Spacer()
switch action {
case .copy(let string):
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
Button(.copyableClickToCopyButton, systemImage: "doc.on.doc") {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(string, forType: .string)
}
@@ -40,10 +40,7 @@ struct ConfigurationItemView<Content: View>: View {
.buttonStyle(.borderless)
case .revealInFinder(let rawPath):
Button(.revealInFinderButton, systemImage: "folder") {
// All foundation-based normalization methods replace this with the container directly.
let processedPath = rawPath.replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
let url = URL(filePath: processedPath)
let folder = url.deletingLastPathComponent().path()
let (processedPath, folder) = rawPath.normalizedPathAndFolder
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
}
.labelStyle(.iconOnly)

View File

@@ -0,0 +1,49 @@
import SwiftUI
struct GettingStartedView: View {
private let instructions = Instructions()
@Binding var selectedInstruction: ConfigurationFileInstructions?
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_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)
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -0,0 +1,87 @@
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)
}
.toolbar {
Button(.setupDoneButton) {
dismiss()
}
}
.hiddenToolbar()
.windowBackgroundStyle(.thinMaterial)
.onAppear {
selectedInstruction = instructions.gettingStarted
}
.frame(minWidth: 400, minHeight: 400)
}
}
struct IntegrationsDetailView: View {
@Binding private var selectedInstruction: ConfigurationFileInstructions?
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_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)
}

View File

@@ -21,9 +21,10 @@ struct SetupView: View {
StepView(
title: .setupAgentTitle,
description: .setupAgentDescription,
detail: .setupAgentActivityMonitorDescription,
systemImage: "lock.laptopcomputer",
) {
setupButton(
SetupButton(
.setupAgentInstallButton,
complete: installed,
width: buttonWidth
@@ -40,7 +41,7 @@ struct SetupView: View {
description: .setupUpdatesDescription,
systemImage: "network.badge.shield.half.filled",
) {
setupButton(
SetupButton(
.setupUpdatesOkButton,
complete: updates,
width: buttonWidth
@@ -54,7 +55,7 @@ struct SetupView: View {
description: .setupIntegrationsDescription,
systemImage: "firewall",
) {
setupButton(
SetupButton(
.setupIntegrationsButton,
complete: integrations,
width: buttonWidth
@@ -63,11 +64,11 @@ struct SetupView: View {
}
}
}
.onPreferenceChange(setupButton.WidthKey.self) { width in
.onPreferenceChange(SetupButton.WidthKey.self) { width in
buttonWidth = width
}
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
.frame(minWidth: 700, maxWidth: .infinity)
.frame(minWidth: 600, maxWidth: .infinity)
HStack {
Spacer()
Button(.setupDoneButton) {
@@ -84,11 +85,14 @@ struct SetupView: View {
integrations = true
}, content: {
IntegrationsView()
.frame(minWidth: 500, minHeight: 400)
})
.frame(idealWidth: 600)
.fixedSize(horizontal: false, vertical: true)
}
}
struct setupButton: View {
struct SetupButton: View {
struct WidthKey: @MainActor PreferenceKey {
@MainActor static var defaultValue: CGFloat? = nil
@@ -144,27 +148,45 @@ struct StepView<Content: View>: View {
let title: LocalizedStringResource
let icon: Image
let description: LocalizedStringResource
let detail: LocalizedStringResource?
let actions: Content
init(title: LocalizedStringResource, description: LocalizedStringResource, systemImage: String, actions: () -> Content) {
init(
title: LocalizedStringResource,
description: LocalizedStringResource,
detail: LocalizedStringResource? = nil,
systemImage: String,
actions: () -> Content
) {
self.title = title
self.icon = Image(systemName: systemImage)
self.description = description
self.detail = detail
self.actions = actions()
}
var body: some View {
HStack(spacing: 20) {
HStack(spacing: 0) {
icon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24)
VStack(alignment: .leading, spacing: 6) {
Spacer()
.frame(width: 20)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.fixedSize(horizontal: false, vertical: true)
.bold()
Text(description)
.fixedSize(horizontal: false, vertical: true)
if let detail {
Text(detail)
.fixedSize(horizontal: false, vertical: true)
.font(.callout)
.italic()
}
Spacer()
}
Spacer(minLength: 20)
actions
}
.padding(20)

View File

@@ -0,0 +1,111 @@
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
}
}
.fixedSize()
}
}
}
} 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))
}
}

View File

@@ -1,350 +0,0 @@
import SwiftUI
struct IntegrationsView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
var body: some View {
NavigationSplitView {
List(selection: $selectedInstruction) {
ForEach(instructions.instructions) { group in
Section(group.name) {
ForEach(group.instructions) { instruction in
Text(instruction.tool)
.padding(.vertical, 8)
.tag(instruction)
}
}
}
}
} detail: {
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
.fauxToolbar {
Button(.setupDoneButton) {
dismiss()
}
.normalButton()
}
}
.onAppear {
selectedInstruction = instructions.gettingStarted
}
.frame(minHeight: 500)
}
}
extension View {
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
modifier(FauxToolbarModifier(toolbarContent: content()))
}
}
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
var toolbarContent: ToolbarContent
func body(content: Content) -> some View {
VStack(alignment: .leading) {
content
Divider()
HStack {
Spacer()
toolbarContent
.padding(.top, 8)
.padding(.trailing, 16)
.padding(.bottom, 16)
}
}
}
}
struct IntegrationsDetailView: View {
@Binding private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_selectedInstruction = selectedInstruction
}
var body: some View {
if let selectedInstruction {
switch selectedInstruction.id {
case .gettingStarted:
Form {
Section(.integrationsGettingStartedTitle) {
Text(.integrationsGettingStartedTitleDescription)
}
Section {
Group {
Text(.integrationsGettingStartedSuggestionSsh)
.onTapGesture {
self.selectedInstruction = instructions.ssh
}
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsGettingStartedSuggestionShell)
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool))
.font(.caption2)
}
.onTapGesture {
self.selectedInstruction = instructions.defaultShell
}
Text(.integrationsGettingStartedSuggestionGit)
.onTapGesture {
self.selectedInstruction = instructions.git
}
}
.foregroundStyle(.link)
} header: {
Text(.integrationsGettingStartedWhatShouldIConfigureTitle)
}
footer: {
Text(.integrationsGettingStartedMultipleConfig)
}
}
.formStyle(.grouped)
case .tool:
Form {
ForEach(selectedInstruction.steps) { stepGroup in
Section {
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
ForEach(stepGroup.steps, id: \.self) { step in
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(step)) {
HStack {
Text(step)
.padding(8)
.font(.system(.subheadline, design: .monospaced))
Spacer()
}
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 6)
.fill(.black.opacity(0.05))
.stroke(.separator, lineWidth: 1)
}
}
}
} footer: {
if let note = stepGroup.note {
Text(note)
.font(.caption)
}
}
}
if let url = selectedInstruction.website {
Section {
Link(destination: url) {
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsWebLink)
.font(.headline)
Text(url.absoluteString)
.font(.caption2)
}
}
}
}
}
.formStyle(.grouped)
case .otherShell:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!)
} header: {
Text(.integrationsCommunityShellListDescription)
.font(.body)
}
}
.formStyle(.grouped)
case .otherApp:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!)
} header: {
Text(.integrationsCommunityAppsListDescription)
.font(.body)
}
}
.formStyle(.grouped)
}
}
}
}
private struct Instructions {
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var defaultShell: ConfigurationFileInstructions {
zsh
}
var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted)
var ssh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: "SSH",
configPath: "~/.ssh/config",
configText: "Host *\n\tIdentityAgent \(socketPath)",
website: URL(string: "https://man.openbsd.org/ssh_config.5")!,
note: "You can tell SSH to use a specific key for a given host. See the web documentation for more details.",
)
}
var git: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: "Git Signing",
steps: [
.init(path: "~/.gitconfig", steps: [
"""
[user]
signingkey = YOUR_PUBLIC_KEY_PATH
[commit]
gpgsign = true
[gpg]
format = ssh
[gpg "ssh"]
allowedSignersFile = ~/.gitallowedsigners
"""
],
note: "If any section (like [user]) already exists, just add the entries in the existing section."
),
.init(
path: "~/.gitallowedsigners",
steps: [
"YOUR_PUBLIC_KEY"
],
note: "~/.gitallowedsigners probably does not exist. You'll need to create it."
),
],
website: URL(string: "https://git-scm.com/docs/git-config")!,
)
}
var zsh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: "zsh",
configPath: "~/.zshrc",
configText: "export SSH_AUTH_SOCK=\(socketPath)"
)
}
var instructions: [ConfigurationGroup] {
[
ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [
gettingStarted
]),
ConfigurationGroup(
name: .integrationsSystemSectionTitle,
instructions: [
ssh,
git,
]
),
ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [
zsh,
ConfigurationFileInstructions(
tool: "bash",
configPath: "~/.bashrc",
configText: "export SSH_AUTH_SOCK=\(socketPath)"
),
ConfigurationFileInstructions(
tool: "fish",
configPath: "~/.config/fish/config.fish",
configText: "set -x SSH_AUTH_SOCK \(socketPath)"
),
ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell),
]),
ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [
ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp),
]),
]
}
}
struct ConfigurationGroup: Identifiable {
let id = UUID()
var name: LocalizedStringResource
var instructions: [ConfigurationFileInstructions] = []
}
struct ConfigurationFileInstructions: Hashable, Identifiable {
struct StepGroup: Hashable, Identifiable {
let path: String
let steps: [String]
let note: String?
var id: String { path }
init(path: String, steps: [String], note: String? = nil) {
self.path = path
self.steps = steps
self.note = note
}
}
var id: ID
var tool: String
var steps: [StepGroup]
var website: URL?
init(tool: String, configPath: String, configText: String, website: URL? = nil, note: String? = nil) {
self.id = .tool(tool)
self.tool = tool
self.steps = [StepGroup(path: configPath, steps: [configText], note: note)]
self.website = website
}
init(tool: String, steps: [StepGroup], website: URL? = nil) {
self.id = .tool(tool)
self.tool = tool
self.steps = steps
self.website = website
}
init(_ name: LocalizedStringResource, id: ID) {
self.id = id
tool = String(localized: name)
self.steps = []
}
enum ID: Identifiable, Hashable {
case gettingStarted
case tool(String)
case otherShell
case otherApp
var id: String {
switch self {
case .gettingStarted:
"getting_started"
case .tool(let name):
name
case .otherShell:
"other_shell"
case .otherApp:
"other_app"
}
}
}
}
#Preview {
IntegrationsView()
.frame(height: 500)
}

View File

@@ -24,7 +24,7 @@ extension View {
}
struct MenuButtonModifier: ViewModifier {
struct ToolbarCircleButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
@@ -40,8 +40,8 @@ struct MenuButtonModifier: ViewModifier {
extension View {
func menuButton() -> some View {
modifier(MenuButtonModifier())
func toolbarCircleButton() -> some View {
modifier(ToolbarCircleButtonModifier())
}
}

View File

@@ -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()
}

View File

@@ -1,6 +1,6 @@
import SwiftUI
struct ToolbarButtonStyle: ButtonStyle {
struct ToolbarStatusButtonStyle: ButtonStyle {
private let lightColor: Color
private let darkColor: Color
@@ -39,6 +39,7 @@ struct ToolbarButtonStyle: ButtonStyle {
} else {
configuration
.label
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
.background(colorScheme == .light ? lightColor : darkColor)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 5))
@@ -55,3 +56,24 @@ struct ToolbarButtonStyle: ButtonStyle {
}
}
}
struct ToolbarButtonStyle: PrimitiveButtonStyle {
var tint: Color = .white.opacity(0.1)
func makeBody(configuration: Configuration) -> some View {
if #available(macOS 26.0, *) {
configuration
.label
.padding(.vertical, 10)
.padding(.horizontal, 12)
.glassEffect(.regular.interactive().tint(tint))
} else {
BorderedButtonStyle().makeBody(configuration: configuration)
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
}
}

View File

@@ -0,0 +1,47 @@
import SwiftUI
struct WindowBackgroundStyleModifier: ViewModifier {
let shapeStyle: any ShapeStyle
func body(content: Content) -> some View {
if #available(macOS 15.0, *) {
content
.containerBackground(
shapeStyle, for: .window
)
} else {
content
}
}
}
extension View {
func windowBackgroundStyle(_ style: some ShapeStyle) -> some View {
modifier(WindowBackgroundStyleModifier(shapeStyle: style))
}
}
struct HiddenToolbarModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 15.0, *) {
content
.toolbarBackgroundVisibility(.hidden, for: .automatic)
} else {
content
}
}
}
extension View {
func hiddenToolbar() -> some View {
modifier(HiddenToolbarModifier())
}
}

View File

@@ -28,7 +28,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
Section {
TextField(String(localized: .createSecretNameLabel), text: $name, prompt: Text(.createSecretNamePlaceholder))
VStack(alignment: .leading, spacing: 10) {
Picker(.createSecretRequireAuthenticationTitle, selection: $authenticationRequirement) {
Picker(.createSecretProtectionLevelTitle, selection: $authenticationRequirement) {
ForEach(authenticationOptions) { option in
HStack {
switch option {
@@ -66,7 +66,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
Text(.createSecretBiometryCurrentWarning)
.padding(.horizontal, 10)
.padding(.vertical, 3)
.background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5))
.boxBackground(color: .red)
}
}
@@ -75,17 +75,30 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
Section {
VStack {
Picker(.createSecretKeyTypeLabel, selection: $keyType) {
ForEach(store.supportedKeyTypes, id: \.self) { option in
ForEach(store.supportedKeyTypes.available, id: \.self) { option in
Text(String(describing: option))
.tag(option)
.font(.caption)
}
Divider()
ForEach(store.supportedKeyTypes.unavailable, id: \.keyType) { option in
VStack {
Button {
} label: {
Text(String(describing: option.keyType))
switch option.reason {
case .macOSUpdateRequired:
Text(.createSecretKeyTypeMacOSUpdateRequiredLabel)
}
}
}
.selectionDisabled()
}
}
if keyType?.algorithm == .mldsa {
Text(.createSecretMldsaWarning)
.padding(.horizontal, 10)
.padding(.vertical, 3)
.background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5))
.boxBackground(color: .orange)
}
}
VStack(alignment: .leading) {
@@ -119,7 +132,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
.padding()
}
.onAppear {
keyType = store.supportedKeyTypes.first
keyType = store.supportedKeyTypes.available.first
}
.formStyle(.grouped)
}
@@ -146,6 +159,6 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
}
#Preview {
CreateSecretView(store: Preview.StoreModifiable()) { _ in }
}
//#Preview {
// CreateSecretView(store: Preview.StoreModifiable()) { _ in }
//}

View File

@@ -5,16 +5,16 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
let store: StoreType
let secret: StoreType.SecretType
let dismissalBlock: (_ renamed: Bool) -> ()
@State private var name: String
@State private var publicKeyAttribution: String
@State var errorText: String?
init(store: StoreType, secret: StoreType.SecretType, dismissalBlock: @escaping (Bool) -> ()) {
@Environment(\.dismiss) var dismiss
init(store: StoreType, secret: StoreType.SecretType) {
self.store = store
self.secret = secret
self.dismissalBlock = dismissalBlock
name = secret.name
publicKeyAttribution = secret.publicKeyAttribution ?? ""
}
@@ -39,7 +39,7 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
}
HStack {
Button(.editCancelButton) {
dismissalBlock(false)
dismiss()
}
.keyboardShortcut(.cancelAction)
Button(.editSaveButton, action: rename)
@@ -58,7 +58,7 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
Task {
do {
try await store.update(secret: secret, name: name, attributes: attributes)
dismissalBlock(true)
dismiss()
} catch {
errorText = error.localizedDescription
}

View File

@@ -28,6 +28,8 @@ 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 {
#Preview {
EmptyStoreImmutableView()
}
#Preview {
EmptyStoreImmutableView()
// .environment(\.justUpdatedChecker, <#T##value: V##V#>)
}
#Preview {
EmptyStoreModifiableView()
}
}
}
#endif

View File

@@ -13,12 +13,7 @@ struct NoStoresView: View {
}
#if DEBUG
struct NoStoresView_Previews: PreviewProvider {
static var previews: some View {
#Preview {
NoStoresView()
}
}
#endif

View File

@@ -21,7 +21,7 @@ struct SecretDetailView<SecretType: Secret>: 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,14 +37,6 @@ struct SecretDetailView<SecretType: Secret>: View {
}
extension URL {
static var agentHomeURL: URL {
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
}
}
#Preview {
SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
}
//#Preview {
// SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
//}

View File

@@ -41,15 +41,12 @@ struct SecretListItemView: View {
deletedSecret(secret)
}
}
.sheet(isPresented: $isRenaming) {
if let modifiable = store as? AnySecretStoreModifiable {
EditSecretView(store: modifiable, secret: secret) { renamed in
isRenaming = false
if renamed {
.sheet(isPresented: $isRenaming, onDismiss: {
renamedSecret(secret)
}
}
}
}
}, content: {
if let modifiable = store as? AnySecretStoreModifiable {
EditSecretView(store: modifiable, secret: secret)
}
})
}
}

View File

@@ -12,9 +12,9 @@ struct StoreListView: View {
}
private func secretRenamed(secret: AnySecret) {
// Toggle so name updates in list.
// Pull new version from store, so we get all updated attributes
activeSecret = nil
activeSecret = secret
activeSecret = storeList.allSecrets.first(where: { $0.id == secret.id })
}
var body: some View {
@@ -28,7 +28,7 @@ struct StoreListView: View {
store: store,
secret: secret,
deletedSecret: secretDeleted,
renamedSecret: secretRenamed
renamedSecret: secretRenamed,
)
}
}
@@ -43,7 +43,11 @@ struct StoreListView: View {
// Do this to avoid a blip.
SecretDetailView(secret: nextDefaultSecret)
} else {
EmptyStoreView(store: storeList.modifiableStore ?? storeList.stores.first)
if let modifiable = storeList.modifiableStore, modifiable.isAvailable {
EmptyStoreView(store: modifiable)
} else {
EmptyStoreView(store: storeList.stores.first(where: \.isAvailable))
}
}
}
.navigationSplitViewStyle(.balanced)

View File

@@ -1,96 +0,0 @@
import SwiftUI
import Brief
struct UpdateDetailView: View {
@Environment(\.updater) var updater: any UpdaterProtocol
let update: Release
var body: some View {
VStack {
Text(.updateVersionName(updateName: update.name)).font(.title)
GroupBox(label: Text(.updateReleaseNotesTitle)) {
ScrollView {
Text(attributedBody)
}
}
HStack {
if !update.critical {
Button(.updateIgnoreButton) {
Task {
await updater.ignore(release: update)
}
}
Spacer()
}
Button(.updateUpdateButton) {
NSWorkspace.shared.open(update.html_url)
}
.keyboardShortcut(.defaultAction)
}
}
.padding()
.frame(maxWidth: 500)
}
var attributedBody: AttributedString {
do {
var text = try AttributedString(
markdown: update.body,
options: .init(
allowsExtendedAttributes: true,
interpretedSyntax: .full,
),
baseURL: URL(string: "https://github.com/maxgoedjen/secretive")!
)
.transformingAttributes(AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self) { key in
let font: Font? = switch key.value?.components.first?.kind {
case .header(level: 1):
Font.title
case .header(level: 2):
Font.title2
case .header(level: 3):
Font.title3
default:
nil
}
if let font {
key.replace(with: AttributeScopes.SwiftUIAttributes.FontAttribute.self, value: font)
}
}
let lineBreak = AttributedString("\n\n")
for run in text.runs.reversed() {
text.insert(lineBreak, at: run.range.lowerBound)
}
return text
} catch {
var text = AttributedString()
for line in update.body.split(whereSeparator: \.isNewline) {
let attributed: AttributedString
let split = line.split(separator: " ")
let unprefixed = split.dropFirst().joined(separator: " ")
if let prefix = split.first {
var container = AttributeContainer()
switch prefix {
case "#":
container.font = .title
case "##":
container.font = .title2
case "###":
container.font = .title3
default:
continue
}
attributed = AttributedString(unprefixed, attributes: container)
} else {
attributed = AttributedString(line + "\n\n")
}
text = text + attributed
}
return text
}
}
}

View File

@@ -0,0 +1,69 @@
import SwiftUI
struct AboutView: View {
var body: some View {
if #available(macOS 15.0, *) {
AboutViewContent()
.containerBackground(
.thinMaterial, for: .window
)
} else {
AboutViewContent()
}
}
}
struct AboutViewContent: View {
@Environment(\.openURL) var openURL
var body: some View {
VStack(spacing: 10) {
HStack {
Image(nsImage: NSApplication.shared.applicationIconImage)
VStack(alignment: .leading) {
Text(verbatim: "Secretive")
.font(.system(.largeTitle, weight: .bold))
Text("**\(Bundle.main.versionNumber)** (\(Bundle.main.buildNumber))")
.fixedSize(horizontal: true, vertical: false)
HStack {
Button(.aboutViewOnGithubButton) {
openURL(URL(string: "https://github.com/maxgoedjen/secretive")!)
}
.normalButton()
Button(.aboutBuildLogButton) {
openURL(Bundle.main.buildLog)
}
.normalButton()
}
}
}
Text(.aboutThanks(contributorsLink: "https://github.com/maxgoedjen/secretive/graphs/contributors", sponsorsLink: "https://github.com/sponsors/maxgoedjen"))
.font(.headline)
Text(.aboutOpenSourceNotice)
.font(.subheadline)
}
.padding(EdgeInsets(top: 10, leading: 30, bottom: 30, trailing: 30))
}
}
private extension Bundle {
var buildLog: URL {
URL(string: infoDictionary!["GitHubBuildLog"] as! String)!
}
var versionNumber: String {
infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
}
var buildNumber: String {
infoDictionary?["CFBundleVersion"] as? String ?? "0.0"
}
}
#Preview {
AboutView()
.frame(width: 500, height: 250)
}

View File

@@ -15,7 +15,6 @@ struct AgentStatusView: View {
struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var body: some View {
Form {
@@ -28,8 +27,8 @@ struct AgentRunningView: View {
)
ConfigurationItemView(
title: .agentDetailsSocketPathTitle,
value: socketPath,
action: .copy(socketPath),
value: URL.socketPath,
action: .copy(URL.socketPath),
)
ConfigurationItemView(
title: .agentDetailsVersionTitle,
@@ -127,7 +126,7 @@ struct AgentNotRunningView: View {
}
}
}
// .primaryButton()
.primaryButton()
} else {
Text(.agentDetailsCouldNotStartError)
.bold()

Some files were not shown because too many files have changed in this diff Show More