Compare commits

...

43 Commits

Author SHA1 Message Date
Max Goedjen
0d16a6ffb1 Clear needs review. 2025-09-11 18:51:01 -07:00
Max Goedjen
95658072e7 Fixed back sides 2025-09-11 18:43:35 -07:00
Max Goedjen
7eeb09b1ec Few more 2025-09-11 18:31:45 -07:00
Max Goedjen
e6f21c13b0 Fix up strings (hopefully) 2025-09-11 18:27:47 -07: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
Max Goedjen
6dc93806a8 Enable GitHub private security issue reporting and update policies (#653)
* Revise security vulnerability reporting process

Updated security reporting instructions in README.md.

* Change vulnerability reporting email to GitHub feature

Updated the vulnerability reporting method to use GitHub's private reporting feature.
2025-09-02 01:46:06 +00:00
Max Goedjen
99a6d48e53 Specify private key usage explicitly (#652) 2025-09-01 02:41:47 +00:00
Max Goedjen
935ac32ea2 Factor out comment writer. (#651) 2025-08-31 23:07:35 +00:00
Max Goedjen
a0a632f245 Save nil if empty string. (#650) 2025-08-31 23:03:59 +00:00
Max Goedjen
51fed9e593 Fix potential timing bug (#649) 2025-08-31 14:22:08 -07:00
Max Goedjen
f652d1d961 Return name as identities comment. (#647) 2025-08-31 20:50:16 +00:00
Max Goedjen
8aacd428b1 Fix deleting key attribution (#648) 2025-08-30 22:41:15 +00:00
88 changed files with 27809 additions and 7206 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
@@ -56,39 +58,34 @@ jobs:
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
- name: Build
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIPs
- name: Create ZIP
run: |
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Xcode_Archive.zip
- name: Notarize
env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
- name: Upload App to Artifacts
id: upload
uses: actions/upload-artifact@v4
with:
name: Secretive.zip
path: Secretive.zip
- name: Attest
id: attest
uses: actions/attest-build-provenance@v2
with:
subject-path: 'Secretive.zip, Xcode_Archive.zip'
subject-name: "Secretive.zip"
subject-digest: ${{ steps.upload.outputs.artifact-digest }}
- name: Create Release
run: |
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
gh release create $TAG_NAME -d -F .github/templates/release.md
gh release upload Secretive.zip
gh release upload Xcode_Archive.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ github.ref }}
RUN_ID: ${{ github.run_id }}
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
- name: Upload App to Artifacts
uses: actions/upload-artifact@v4
with:
name: Secretive.zip
path: Secretive.zip
- name: Upload Archive to Artifacts
uses: actions/upload-artifact@v4
with:
name: Xcode_Archive.zip
path: Xcode_Archive.zip

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

@@ -61,4 +61,4 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
## Security
If you discover any vulnerabilities in this project, please notify [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with the subject containing "SECRETIVE SECURITY."
Secretive's security policy is detailed in [SECURITY.md](SECURITY.md). To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)

View File

@@ -24,4 +24,4 @@ The latest version on the [Releases page](https://github.com/maxgoedjen/secretiv
## Reporting a Vulnerability
If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com with the subject containing "SECRETIVE SECURITY."
To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability)

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

@@ -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")
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
Task {
await checkForUpdates()
}
}
Task {
if checkOnLaunch {
try await checkForUpdates()
}
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
}
@@ -89,9 +72,8 @@ extension Agent {
for secret in secrets {
let keyBlob = publicKeyWriter.data(secret: secret)
let curveData = publicKeyWriter.openSSHIdentifier(for: secret.keyType)
keyData.append(keyBlob.lengthAndData)
keyData.append(curveData.lengthAndData)
keyData.append(publicKeyWriter.comment(secret: secret).lengthAndData)
count += 1
if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) {
@@ -102,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
}
@@ -111,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)
@@ -170,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,10 +1,11 @@
import Foundation
import OSLog
import SecretKit
/// Manages storage and lookup for OpenSSH certificates.
public actor OpenSSHCertificateHandler: Sendable {
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
private let writer = OpenSSHPublicKeyWriter()
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
@@ -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

@@ -78,7 +78,6 @@ extension SocketController {
provenance = SigningRequestTracer().provenance(from: fileHandle)
(messages, messagesContinuation) = AsyncStream.makeStream()
Task { [messagesContinuation, logger] in
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) {
let data = fileHandle.availableData
guard !data.isEmpty else {
@@ -91,6 +90,9 @@ extension SocketController {
logger.debug("Socket controller yielded data.")
}
}
Task {
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
}
}
/// Writes new data to the socket.
@@ -131,22 +133,21 @@ private extension SocketPort {
convenience init(path: String) {
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
var len: Int = 0
withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
path.withCString { cstring in
len = strlen(cstring)
strncpy(pointer, cstring, len)
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
}
}
addr.sun_len = UInt8(len+2)
var data: Data!
withUnsafePointer(to: &addr) { pointer in
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
}
// 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)
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

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

@@ -31,18 +31,7 @@ public struct OpenSSHPublicKeyWriter: Sendable {
/// Generates an OpenSSH string representation of the secret.
/// - Returns: OpenSSH string representation of the secret.
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String {
let resolvedComment: String
if let comment = secret.publicKeyAttribution {
resolvedComment = comment
} else {
let dashedKeyName = secret.name.replacingOccurrences(of: " ", with: "-")
let dashedHostName = ["secretive", Host.current().localizedName, "local"]
.compactMap { $0 }
.joined(separator: ".")
.replacingOccurrences(of: " ", with: "-")
resolvedComment = "\(dashedKeyName)@\(dashedHostName)"
}
return [openSSHIdentifier(for: secret.keyType), data(secret: secret).base64EncodedString(), resolvedComment]
return [openSSHIdentifier(for: secret.keyType), data(secret: secret).base64EncodedString(), comment(secret: secret)]
.compactMap { $0 }
.joined(separator: " ")
}
@@ -65,6 +54,19 @@ public struct OpenSSHPublicKeyWriter: Sendable {
.joined(separator: ":")
}
public func comment<SecretType: Secret>(secret: SecretType) -> String {
if let comment = secret.publicKeyAttribution {
return comment
} else {
let dashedKeyName = secret.name.replacingOccurrences(of: " ", with: "-")
let dashedHostName = ["secretive", Host.current().localizedName, "local"]
.compactMap { $0 }
.joined(separator: ".")
.replacingOccurrences(of: " ", with: "-")
return "\(dashedKeyName)@\(dashedHostName)"
}
}
}
extension OpenSSHPublicKeyWriter {
@@ -95,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

@@ -5,12 +5,12 @@ import OSLog
public final class PublicKeyFileStoreController: Sendable {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
private let directory: String
private let directory: URL
private let keyWriter = OpenSSHPublicKeyWriter()
/// Initializes a PublicKeyFileStoreController.
public init(homeDirectory: String) {
directory = homeDirectory.appending("/PublicKeys")
public init(homeDirectory: URL) {
directory = homeDirectory.appending(component: "PublicKeys")
}
/// Writes out the keys specified to disk.
@@ -20,16 +20,17 @@ public final class PublicKeyFileStoreController: Sendable {
logger.log("Writing public keys to disk")
if clear {
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
let untracked = Set(fullPathContents)
.subtracting(validPaths)
for path in untracked {
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
// string instead of fileURLWithPath since we're already using fileURL format.
try? FileManager.default.removeItem(at: URL(string: path)!)
}
}
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
for secret in secrets {
let path = publicKeyPath(for: secret)
let data = Data(keyWriter.openSSHString(secret: secret).utf8)
@@ -44,14 +45,14 @@ public final class PublicKeyFileStoreController: Sendable {
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending("/").appending("\(minimalHex).pub")
return directory.appending(component: "\(minimalHex).pub").path()
}
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
public var hasAnyCertificates: Bool {
do {
return try FileManager.default
.contentsOfDirectory(atPath: directory)
.contentsOfDirectory(atPath: directory.path())
.filter { $0.hasSuffix("-cert.pub") }
.isEmpty == false
} catch {
@@ -65,7 +66,7 @@ public final class PublicKeyFileStoreController: Sendable {
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending("/").appending("\(minimalHex)-cert.pub")
return directory.appending(component: "\(minimalHex)-cert.pub").path()
}
}

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,25 +40,29 @@ 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
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
logger.log("Skipping \(name), public key already present. Marking as migrated.")
do {
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
logger.log("Skipping \(name), public key already present. Marking as migrated.")
try markMigrated(secret: secret, oldID: id)
continue
}
logger.log("Migrating \(name).")
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
logger.log("Migrated \(name).")
try markMigrated(secret: secret, oldID: id)
continue
migratedAny = true
} catch {
logger.error("Failed to migrate \(name): \(error).")
}
logger.log("Migrating \(name).")
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
logger.log("Migrated \(name).")
try markMigrated(secret: secret, oldID: id)
migrated = true
}
if migrated {
if migratedAny {
store.reloadSecrets()
}
}

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) {
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
} else {
newContext.localizedReason = String(localized: .authContextPersistForDurationUnknown(secretName: secret.name))
}
let durationString = formatter.string(from: duration)!
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
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)
}
@@ -112,7 +112,7 @@ extension SecureEnclave {
var accessError: SecurityError?
let flags: SecAccessControlCreateFlags = switch attributes.authentication {
case .notRequired:
[]
[.privateKeyUsage]
case .presenceRequired:
[.userPresence, .privateKeyUsage]
case .biometryCurrent:
@@ -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
@@ -214,7 +214,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,70 @@
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 {
reply(nil, error)
}
}
}
}
}
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

@@ -21,7 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}()
private let updater = Updater(checkOnLaunch: true)
private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
private lazy var agent: Agent = {
Agent(storeList: storeList, witness: notifier)
}()
@@ -34,11 +34,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
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 agentResponse = try await agent.handle(data: message, provenance: session.provenance)
let request = try await inputParser.parse(data: message)
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
try await session.write(agentResponse)
}
} catch {
@@ -58,7 +60,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
updater.update
} onChange: { [updater, notifier] in
Task {
guard !updater.testBuild else { return }
guard !updater.currentVersion.isTestBuild else { return }
await notifier.notify(update: updater.update!) { release in
await updater.ignore(release: release)
}

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,23 @@
import Foundation
import SecretAgentKit
import Brief
import XPCWrappers
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
private let session: XPCTypedSession<SSHAgent.Request, SSHAgentInputParser.AgentParsingError>
public init() async throws {
session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretAgentInputParser", warmup: true)
}
public func parse(data: Data) async throws -> SSHAgent.Request {
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

@@ -25,7 +25,13 @@
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 */; };
@@ -36,9 +42,19 @@
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
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 */; };
@@ -49,8 +65,12 @@
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */; };
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; };
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
/* End PBXBuildFile section */
@@ -63,9 +83,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;
@@ -103,10 +182,16 @@
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; };
@@ -120,9 +205,17 @@
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>"; };
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
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>"; };
@@ -138,8 +231,12 @@
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; };
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = "<group>"; };
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = "<group>"; };
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -156,6 +253,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;
@@ -179,6 +293,56 @@
path = Helpers;
sourceTree = "<group>";
};
504788ED2E681EB200B4556F /* Styles */ = {
isa = PBXGroup;
children = (
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
);
path = Styles;
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 = (
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
);
path = Views;
sourceTree = "<group>";
};
50617D7623FCE48D0099B055 = {
isa = PBXGroup;
children = (
@@ -186,6 +350,8 @@
50617D8123FCE48E0099B055 /* Secretive */,
50A3B78B24026B7500D209EA /* SecretAgent */,
508A58AF241E144C0069DC07 /* Config */,
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */,
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */,
50617D8023FCE48E0099B055 /* Products */,
5099A08B240243730062B6F2 /* Frameworks */,
);
@@ -196,6 +362,8 @@
children = (
50617D7F23FCE48E0099B055 /* Secretive.app */,
50A3B78A24026B7500D209EA /* SecretAgent.app */,
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */,
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */,
);
name = Products;
sourceTree = "<group>";
@@ -229,6 +397,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 = (
@@ -241,20 +430,10 @@
508A58B0241ED1C40069DC07 /* Views */ = {
isa = PBXGroup;
children = (
50617D8423FCE48E0099B055 /* ContentView.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
50153E21250DECA300525160 /* SecretListItemView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
5066A6C12516F303004B5A36 /* SetupView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
504788EF2E681ED700B4556F /* Configuration */,
504788EE2E681EC300B4556F /* Secrets */,
504788ED2E681EB200B4556F /* Styles */,
504788F02E681F0100B4556F /* Views */,
);
path = Views;
sourceTree = "<group>";
@@ -262,11 +441,11 @@
508A58B1241ED1EA0069DC07 /* Controllers */ = {
isa = PBXGroup;
children = (
504788EB2E680DC400B4556F /* URLs.swift */,
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
);
path = Controllers;
sourceTree = "<group>";
@@ -283,6 +462,7 @@
children = (
50020BAF24064869003D4025 /* AppDelegate.swift */,
5018F54E24064786002EB505 /* Notifier.swift */,
501578122E6C0479004A37D0 /* XPCInputParser.swift */,
50A3B79524026B7600D209EA /* Main.storyboard */,
50A3B79824026B7600D209EA /* Info.plist */,
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
@@ -312,11 +492,14 @@
50617D7D23FCE48D0099B055 /* Resources */,
50617DBF23FCE4AB0099B055 /* Embed Frameworks */,
50C385AF240E438B00AF2719 /* CopyFiles */,
501577C92E6BC5B4004A37D0 /* Embed XPC Services */,
);
buildRules = (
);
dependencies = (
50142167278126B500BBAA70 /* PBXTargetDependency */,
50692D1C2E6FDB880043C7BB /* PBXTargetDependency */,
50692E5A2E6FF9D20043C7BB /* PBXTargetDependency */,
);
name = Secretive;
packageProductDependencies = (
@@ -329,6 +512,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" */;
@@ -337,10 +561,14 @@
50A3B78724026B7500D209EA /* Frameworks */,
50A3B78824026B7500D209EA /* Resources */,
50A5C18E240E4B4B00E2996C /* Embed Frameworks */,
501577D22E6BC5D4004A37D0 /* Embed XPC Services */,
);
buildRules = (
);
dependencies = (
501577D42E6BC5DD004A37D0 /* PBXTargetDependency */,
50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */,
50692E722E6FFA6E0043C7BB /* PBXTargetDependency */,
);
name = SecretAgent;
packageProductDependencies = (
@@ -361,13 +589,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;
};
@@ -397,6 +631,8 @@
targets = (
50617D7E23FCE48D0099B055 /* Secretive */,
50A3B78924026B7500D209EA /* SecretAgent */,
50692D112E6FDB880043C7BB /* SecretiveUpdater */,
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
);
};
/* End PBXProject section */
@@ -414,6 +650,20 @@
);
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;
@@ -433,26 +683,34 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */,
50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */,
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
50617D8323FCE48E0099B055 /* App.swift in Sources */,
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
@@ -460,12 +718,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 = (
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -477,6 +754,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 */
@@ -561,6 +862,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;
@@ -628,6 +931,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;
@@ -647,10 +952,18 @@
ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readwrite;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -679,10 +992,18 @@
ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readwrite;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -696,6 +1017,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 = 26.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 = 26.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 = 26.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 = 26.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 = 26.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 = 26.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 */;
@@ -766,6 +1306,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;
@@ -783,10 +1325,18 @@
ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readwrite;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -809,8 +1359,17 @@
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -835,8 +1394,17 @@
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -862,8 +1430,17 @@
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -900,6 +1477,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 = (
@@ -949,6 +1546,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

@@ -25,6 +25,9 @@ extension EnvironmentValues {
}()
@Entry var updater: any UpdaterProtocol = _updater
private static let _justUpdatedChecker = JustUpdatedChecker()
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
@MainActor var secretStoreList: SecretStoreList {
EnvironmentValues._secretStoreList
}
@@ -33,10 +36,11 @@ extension EnvironmentValues {
@main
struct Secretive: App {
private let justUpdatedChecker = JustUpdatedChecker()
@Environment(\.agentStatusChecker) var agentStatusChecker
@Environment(\.justUpdatedChecker) var justUpdatedChecker
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false
@State private var showingIntegrations = false
@State private var showingCreation = false
@SceneBuilder var body: some Scene {
@@ -51,15 +55,23 @@ struct Secretive: App {
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
guard hasRunSetup else { return }
agentStatusChecker.check()
if agentStatusChecker.running && justUpdatedChecker.justUpdated {
if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild {
// Relaunch the agent, since it'll be running from earlier update still
reinstallAgent()
} else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild {
forceLaunchAgent()
}
}
.sheet(isPresented: $showingIntegrations) {
IntegrationsView()
}
}
.commands {
CommandGroup(before: CommandGroupPlacement.appSettings) {
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
showingIntegrations = true
}
}
CommandGroup(after: CommandGroupPlacement.newItem) {
Button(.appMenuNewSecretButton) {
showingCreation = true
@@ -71,11 +83,6 @@ struct Secretive: App {
NSWorkspace.shared.open(Constants.helpURL)
}
}
CommandGroup(after: .help) {
Button(.appMenuSetupButton) {
showingSetup = true
}
}
SidebarCommands()
}
}
@@ -85,9 +92,8 @@ struct Secretive: App {
extension Secretive {
private func reinstallAgent() {
justUpdatedChecker.check()
Task {
await LaunchAgentController().install()
_ = await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check()
if !agentStatusChecker.running {

View File

@@ -6,12 +6,14 @@ import Observation
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
var running: Bool { get }
var developmentBuild: Bool { get }
var process: NSRunningApplication? { get }
func check()
}
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
var running: Bool = false
var process: NSRunningApplication? = nil
nonisolated init() {
Task { @MainActor in
@@ -20,32 +22,39 @@ import Observation
}
func check() {
running = instanceSecretAgentProcess != nil
process = instanceSecretAgentProcess
running = process != nil
}
// All processes, including ones from older versions, etc
var secretAgentProcesses: [NSRunningApplication] {
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID)
var allSecretAgentProcesses: [NSRunningApplication] {
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.agentBundleID)
}
// The process corresponding to this instance of Secretive
var instanceSecretAgentProcess: NSRunningApplication? {
let agents = secretAgentProcesses
// FIXME: CHECK VERSION
let agents = allSecretAgentProcesses
for agent in agents {
guard let url = agent.bundleURL else { continue }
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) {
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) {
return agent
}
}
return nil
}
// Whether Secretive is being run in an Xcode environment.
var developmentBuild: Bool {
Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode")
Bundle.main.bundleURL.isXcodeURL
}
}
extension URL {
var isXcodeURL: Bool {
absoluteString.contains("/Library/Developer/Xcode")
}
}

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() {
check()
nonisolated init() {
Task { @MainActor in
check()
}
}
func check() {
let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None"
private func check() {
let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String
let lastOS = UserDefaults.standard.object(forKey: Constants.previousOSVersionUserDefaultsKey) as? String
let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
let osRaw = ProcessInfo.processInfo.operatingSystemVersion
let currentOS = "\(osRaw.majorVersion).\(osRaw.minorVersion).\(osRaw.patchVersion)"
UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey)
justUpdated = lastBuild != currentBuild
UserDefaults.standard.set(currentOS, forKey: Constants.previousOSVersionUserDefaultsKey)
justUpdatedBuild = lastBuild != currentBuild
// To prevent this showing on first lauch for every user, only show if lastBuild is non-nil.
justUpdatedOS = lastBuild != nil && lastOS != currentOS
}
@@ -28,6 +38,7 @@ extension JustUpdatedChecker {
enum Constants {
static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild"
static let previousOSVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastOS"
}
}

View File

@@ -8,16 +8,28 @@ struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install() async {
func install() async -> Bool {
logger.debug("Installing agent")
_ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new?
try? await Task.sleep(for: .seconds(1))
await MainActor.run {
_ = setEnabled(true)
let result = await MainActor.run {
setEnabled(true)
}
try? await Task.sleep(for: .seconds(1))
return result
}
func uninstall() async -> Bool {
logger.debug("Uninstalling agent")
try? await Task.sleep(for: .seconds(1))
let result = await MainActor.run {
setEnabled(false)
}
try? await Task.sleep(for: .seconds(1))
return result
}
func forceLaunch() async -> Bool {
@@ -28,6 +40,7 @@ struct LaunchAgentController {
do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched")
try? await Task.sleep(for: .seconds(1))
return true
} catch {
logger.error("Error force launching \(error.localizedDescription)")
@@ -36,7 +49,7 @@ struct LaunchAgentController {
}
private func setEnabled(_ enabled: Bool) -> Bool {
let service = SMAppService.loginItem(identifier: Bundle.main.agentBundleID)
let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
do {
if enabled {
try service.register()

View File

@@ -1,63 +0,0 @@
import Foundation
import Cocoa
import SecretKit
struct ShellConfigurationController {
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var shellInstructions: [ShellConfigInstruction] {
[
ShellConfigInstruction(shell: "global",
shellConfigDirectory: "~/.ssh/",
shellConfigFilename: "config",
text: "Host *\n\tIdentityAgent \(socketPath)"),
ShellConfigInstruction(shell: "zsh",
shellConfigDirectory: "~/",
shellConfigFilename: ".zshrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "bash",
shellConfigDirectory: "~/",
shellConfigFilename: ".bashrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "fish",
shellConfigDirectory: "~/.config/fish",
shellConfigFilename: "config.fish",
text: "set -x SSH_AUTH_SOCK \(socketPath)"),
]
}
@MainActor func addToShell(shellInstructions: ShellConfigInstruction) -> Bool {
let openPanel = NSOpenPanel()
// This is sync, so no need to strongly retain
let delegate = Delegate(name: shellInstructions.shellConfigFilename)
openPanel.delegate = delegate
openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically."
openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)"
openPanel.canChooseFiles = true
openPanel.canChooseDirectories = false
openPanel.showsHiddenFiles = true
openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory)
openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename
openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText]
openPanel.runModal()
guard let fileURL = openPanel.urls.first else { return false }
let handle: FileHandle
do {
handle = try FileHandle(forUpdating: fileURL)
guard let existing = try handle.readToEnd(),
let existingString = String(data: existing, encoding: .utf8) else { return false }
guard !existingString.contains(shellInstructions.text) else {
return true
}
try handle.seekToEnd()
} catch {
return false
}
handle.write(Data("\n# Secretive Config\n\(shellInstructions.text)\n".utf8))
return true
}
}

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,7 +1,11 @@
import Foundation
extension Bundle {
public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!}
public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!}
public static var agentBundleID: String {
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent")
}
public static var hostBundleID: String {
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host")
}
}

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

@@ -1,12 +1,15 @@
import Foundation
import AppKit
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
let running: Bool
let process: NSRunningApplication?
let developmentBuild = false
init(running: Bool = true) {
init(running: Bool = true, process: NSRunningApplication? = nil) {
self.running = running
self.process = process
}
func check() {

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

@@ -1,24 +0,0 @@
import SwiftUI
struct PrimaryButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
}
}
extension View {
func primary() -> some View {
modifier(PrimaryButtonModifier())
}
}

View File

@@ -0,0 +1,56 @@
import SwiftUI
struct ConfigurationItemView<Content: View>: View {
enum Action: Hashable {
case copy(String)
case revealInFinder(String)
}
let title: LocalizedStringResource
let content: Content
let action: Action?
init(title: LocalizedStringResource, value: String, action: Action? = nil) where Content == Text {
self.title = title
self.content = Text(value)
.font(.subheadline)
.foregroundStyle(.secondary)
self.action = action
}
init(title: LocalizedStringResource, action: Action? = nil, content: () -> Content) {
self.title = title
self.content = content()
self.action = action
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(title)
Spacer()
switch action {
case .copy(let string):
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(string, forType: .string)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case .revealInFinder(let rawPath):
Button(.revealInFinderButton, systemImage: "folder") {
let (processedPath, folder) = rawPath.normalizedPathAndFolder
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case nil:
EmptyView()
}
}
content
}
}
}

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,115 @@
import SwiftUI
struct IntegrationsView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
var body: some View {
NavigationSplitView {
List(selection: $selectedInstruction) {
ForEach(instructions.instructions) { group in
Section(group.name) {
ForEach(group.instructions) { instruction in
Text(instruction.tool)
.padding(.vertical, 8)
.tag(instruction)
}
}
}
}
} detail: {
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
.fauxToolbar {
Button(.setupDoneButton) {
dismiss()
}
.normalButton()
}
}
.onAppear {
selectedInstruction = instructions.gettingStarted
}
.frame(minHeight: 500)
}
}
extension View {
func fauxToolbar<Content: 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, spacing: 0) {
content
Divider()
HStack {
Spacer()
toolbarContent
.padding(.top, 8)
.padding(.trailing, 16)
.padding(.bottom, 16)
}
}
}
}
struct IntegrationsDetailView: View {
@Binding private var selectedInstruction: ConfigurationFileInstructions?
init(selectedInstruction: Binding<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

@@ -0,0 +1,201 @@
import SwiftUI
struct SetupView: View {
@Environment(\.dismiss) private var dismiss
@Binding var setupComplete: Bool
@State var showingIntegrations = false
@State var buttonWidth: CGFloat?
@State var installed = false
@State var updates = false
@State var integrations = false
var allDone: Bool {
installed && updates && integrations
}
var body: some View {
VStack {
VStack(alignment: .leading, spacing: 0) {
StepView(
title: .setupAgentTitle,
description: .setupAgentDescription,
detail: .setupAgentActivityMonitorDescription,
systemImage: "lock.laptopcomputer",
) {
SetupButton(
.setupAgentInstallButton,
complete: installed,
width: buttonWidth
) {
installed = true
Task {
await LaunchAgentController().install()
}
}
}
Divider()
StepView(
title: .setupUpdatesTitle,
description: .setupUpdatesDescription,
systemImage: "network.badge.shield.half.filled",
) {
SetupButton(
.setupUpdatesOkButton,
complete: updates,
width: buttonWidth
) {
updates = true
}
}
Divider()
StepView(
title: .setupIntegrationsTitle,
description: .setupIntegrationsDescription,
systemImage: "firewall",
) {
SetupButton(
.setupIntegrationsButton,
complete: integrations,
width: buttonWidth
) {
showingIntegrations = true
}
}
}
.onPreferenceChange(SetupButton.WidthKey.self) { width in
buttonWidth = width
}
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
.frame(minWidth: 600, maxWidth: .infinity)
HStack {
Spacer()
Button(.setupDoneButton) {
setupComplete = true
dismiss()
}
.disabled(!allDone)
.primaryButton()
}
}
.interactiveDismissDisabled()
.padding()
.sheet(isPresented: $showingIntegrations, onDismiss: {
integrations = true
}, content: {
IntegrationsView()
})
}
}
struct SetupButton: View {
struct WidthKey: @MainActor PreferenceKey {
@MainActor static var defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
if let next = nextValue(), next > (value ?? -1) {
value = next
}
}
}
let label: LocalizedStringResource
let complete: Bool
let action: () -> Void
let width: CGFloat?
@State var currentWidth: CGFloat?
init(_ label: LocalizedStringResource, complete: Bool, width: CGFloat? = nil, action: @escaping () -> Void) {
self.label = label
self.complete = complete
self.action = action
self.width = width
}
var body: some View {
Button(action: action) {
HStack(spacing: 6) {
if complete {
Text(.setupStepCompleteButton)
Image(systemName: "checkmark.circle.fill")
} else {
Text(label)
}
}
.frame(width: width)
.padding(.vertical, 2)
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { newValue in
currentWidth = newValue
}
}
.preference(key: WidthKey.self, value: currentWidth)
.primaryButton()
.disabled(complete)
.tint(complete ? .green : nil)
}
}
struct StepView<Content: View>: View {
let title: LocalizedStringResource
let icon: Image
let description: LocalizedStringResource
let detail: LocalizedStringResource?
let 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: 0) {
icon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24)
Spacer()
.frame(width: 20)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.bold()
Text(description)
if let detail {
Text(detail)
.font(.callout)
.italic()
}
}
Spacer(minLength: 20)
actions
}
.padding(20)
}
}
extension SetupView {
enum Constants {
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
}
}
#Preview {
SetupView(setupComplete: .constant(false))
}

View File

@@ -0,0 +1,110 @@
import SwiftUI
import SecretKit
struct ToolConfigurationView: View {
private let instructions = Instructions()
let selectedInstruction: ConfigurationFileInstructions
@Environment(\.secretStoreList) private var secretStoreList
@State var creating = false
@State var selectedSecret: AnySecret?
init(selectedInstruction: ConfigurationFileInstructions) {
self.selectedInstruction = selectedInstruction
}
var body: some View {
Form {
if selectedInstruction.requiresSecret {
if secretStoreList.allSecrets.isEmpty {
Section {
Text(.integrationsConfigureUsingSecretEmptyCreate)
if let store = secretStoreList.modifiableStore {
HStack {
Spacer()
Button(.createSecretTitle) {
creating = true
}
.sheet(isPresented: $creating) {
CreateSecretView(store: store) { created in
selectedSecret = created
}
}
}
}
}
} else {
Section {
Picker(.integrationsConfigureUsingSecretSecretTitle, selection: $selectedSecret) {
if selectedSecret == nil {
Text(.integrationsConfigureUsingSecretNoSecret)
.tag(nil as (AnySecret?))
}
ForEach(secretStoreList.allSecrets) { secret in
Text(secret.name)
.tag(secret)
}
}
} header: {
Text(.integrationsConfigureUsingSecretHeader)
}
.onAppear {
selectedSecret = secretStoreList.allSecrets.first
}
}
}
ForEach(selectedInstruction.steps) { stepGroup in
Section {
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
ForEach(stepGroup.steps, id: \.self.key) { step in
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) {
HStack {
Text(placeholdersReplaced(text: String(localized: step)))
.padding(8)
.font(.system(.subheadline, design: .monospaced))
Spacer()
}
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 6)
.fill(.black.opacity(0.05))
.stroke(.separator, lineWidth: 1)
}
}
}
} footer: {
if let note = stepGroup.note {
Text(note)
.font(.caption)
}
}
}
if let url = selectedInstruction.website {
Section {
Link(destination: url) {
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsWebLink)
.font(.headline)
Text(url.absoluteString)
.font(.caption2)
}
}
}
}
}
.formStyle(.grouped)
}
func placeholdersReplaced(text: String) -> String {
guard let selectedSecret else { return text }
let writer = OpenSSHPublicKeyWriter()
let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
return text
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret))
.replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret))
}
}

View File

@@ -4,13 +4,15 @@ import SecretKit
struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@State var store: StoreType
@Binding var showing: Bool
@Environment(\.dismiss) private var dismiss
var createdSecret: (AnySecret?) -> Void
@State private var name = ""
@State private var keyAttribution = ""
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
@State private var keyType: KeyType?
@State var advanced = false
@State var errorText: String?
private var authenticationOptions: [AuthenticationRequirement] {
if advanced || authenticationRequirement == .biometryCurrent {
@@ -26,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 {
@@ -64,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)
}
}
@@ -83,7 +85,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
Text(.createSecretMldsaWarning)
.padding(.horizontal, 10)
.padding(.vertical, 3)
.background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5))
.boxBackground(color: .orange)
}
}
VStack(alignment: .leading) {
@@ -94,16 +96,24 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
}
}
}
if let errorText {
Section {
} footer: {
Text(verbatim: errorText)
.errorStyle()
}
}
}
HStack {
Toggle(.createSecretAdvancedLabel, isOn: $advanced)
.toggleStyle(.button)
Spacer()
Button(.createSecretCancelButton, role: .cancel) {
showing = false
dismiss()
}
Button(.createSecretCreateButton, action: save)
.primary()
.keyboardShortcut(.return)
.primaryButton()
.disabled(name.isEmpty)
}
.padding()
@@ -117,20 +127,25 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
func save() {
let attribution = keyAttribution.isEmpty ? nil : keyAttribution
Task {
try! await store.create(
name: name,
attributes: .init(
keyType: keyType!,
authentication: authenticationRequirement,
publicKeyAttribution: attribution
do {
let new = try await store.create(
name: name,
attributes: .init(
keyType: keyType!,
authentication: authenticationRequirement,
publicKeyAttribution: attribution
)
)
)
showing = false
createdSecret(AnySecret(new))
dismiss()
} catch {
errorText = error.localizedDescription
}
}
}
}
#Preview {
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
}
//#Preview {
// CreateSecretView(store: Preview.StoreModifiable()) { _ in }
//}

View File

@@ -28,8 +28,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier {
TextField(secret.name, text: $confirmedSecretName)
if let errorText {
Text(verbatim: errorText)
.foregroundStyle(.red)
.font(.callout)
.errorStyle()
}
Button(.deleteConfirmationDeleteButton, action: delete)
.disabled(confirmedSecretName != secret.name)

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 ?? ""
}
@@ -30,21 +30,22 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
if let errorText {
Text(verbatim: errorText)
.foregroundStyle(.red)
.font(.callout)
} footer: {
if let errorText {
Text(verbatim: errorText)
.errorStyle()
}
}
}
HStack {
Button(.editCancelButton) {
dismiss()
}
.keyboardShortcut(.cancelAction)
Button(.editSaveButton, action: rename)
.disabled(name.isEmpty)
.keyboardShortcut(.return)
Button(.editCancelButton) {
dismissalBlock(false)
}
.keyboardShortcut(.cancelAction)
.primaryButton()
}
.padding()
}
@@ -53,13 +54,11 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
func rename() {
var attributes = secret.attributes
if !publicKeyAttribution.isEmpty {
attributes.publicKeyAttribution = publicKeyAttribution
}
attributes.publicKeyAttribution = publicKeyAttribution.isEmpty ? nil : publicKeyAttribution
Task {
do {
try await store.update(secret: secret, name: name, attributes: attributes)
dismissalBlock(true)
dismiss()
} catch {
errorText = error.localizedDescription
}

View File

@@ -27,7 +27,9 @@ struct EmptyStoreImmutableView: View {
}
struct EmptyStoreModifiableView: View {
@Environment(\.justUpdatedChecker) var justUpdatedChecker
var body: some View {
GeometryReader { windowGeometry in
VStack {
@@ -51,21 +53,35 @@ struct EmptyStoreModifiableView: View {
}.frame(height: (windowGeometry.size.height/2) - 20).padding()
Text(.emptyStoreModifiableClickHereTitle).bold()
Text(.emptyStoreModifiableClickHereDescription)
if justUpdatedChecker.justUpdatedOS {
Spacer()
.frame(height: 20)
VStack(spacing: 10) {
Text(.emptyStoreModifiableEmptyOsWarningTitle)
.font(.title2)
.bold()
Text(.emptyStoreModifiableEmptyOsWarningDescription)
.fixedSize(horizontal: false, vertical: true)
.bold()
}
.padding()
.boxBackground(color: .orange)
.padding()
}
Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
#if DEBUG
struct EmptyStoreModifiableView_Previews: PreviewProvider {
static var previews: some View {
Group {
EmptyStoreImmutableView()
EmptyStoreModifiableView()
}
}
#Preview {
EmptyStoreImmutableView()
}
#Preview {
EmptyStoreImmutableView()
// .environment(\.justUpdatedChecker, <#T##value: V##V#>)
}
#Preview {
EmptyStoreModifiableView()
}
#endif

View File

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

View File

@@ -6,8 +6,8 @@ struct SecretDetailView<SecretType: Secret>: View {
let secret: SecretType
private let keyWriter = OpenSSHPublicKeyWriter()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID))
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
var body: some View {
ScrollView {
Form {
@@ -21,7 +21,7 @@ struct SecretDetailView<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,12 +37,6 @@ struct SecretDetailView<SecretType: Secret>: View {
}
#if DEBUG
struct SecretDetailView_Previews: PreviewProvider {
static var previews: some View {
SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0])
}
}
#endif
//#Preview {
// SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
//}

View File

@@ -41,15 +41,12 @@ struct SecretListItemView: View {
deletedSecret(secret)
}
}
.sheet(isPresented: $isRenaming) {
.sheet(isPresented: $isRenaming, onDismiss: {
renamedSecret(secret)
}, content: {
if let modifiable = store as? AnySecretStoreModifiable {
EditSecretView(store: modifiable, secret: secret) { renamed in
isRenaming = false
if renamed {
renamedSecret(secret)
}
}
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,297 +0,0 @@
import SwiftUI
struct SetupView: View {
@State var stepIndex = 0
@Binding var visible: Bool
@Binding var setupComplete: Bool
var body: some View {
GeometryReader { proxy in
VStack {
StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width)
GeometryReader { _ in
HStack(spacing: 0) {
SecretAgentSetupView(buttonAction: advance)
.frame(width: proxy.size.width)
SSHAgentSetupView(buttonAction: advance)
.frame(width: proxy.size.width)
UpdaterExplainerView {
visible = false
setupComplete = true
}
.frame(width: proxy.size.width)
}
.offset(x: -proxy.size.width * Double(stepIndex), y: 0)
}
}
}
.frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500)
}
func advance() {
withAnimation(.spring()) {
stepIndex += 1
}
}
}
struct StepView: View {
let numberOfSteps: Int
let currentStep: Int
// Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7
let width: Double
var body: some View {
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.blue)
.frame(height: 5)
Rectangle()
.foregroundColor(.green)
.frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5)
HStack {
ForEach(Array(0..<numberOfSteps), id: \.self) { index in
ZStack {
if currentStep > index {
Circle()
.foregroundColor(.green)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
Text(.setupStepCompleteSymbol)
.foregroundColor(.white)
.bold()
} else {
Circle()
.foregroundColor(.blue)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
if currentStep == index {
Circle()
.strokeBorder(Color.white, lineWidth: 3)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
}
Text(String(describing: index + 1))
.foregroundColor(.white)
.bold()
}
}
if index < numberOfSteps - 1 {
Spacer(minLength: 30)
}
}
}
}.padding(Constants.padding)
}
}
extension StepView {
enum Constants {
static let padding: Double = 15
static let circleWidth: Double = 30
}
}
struct SetupStepView<Content> : View where Content : View {
let title: LocalizedStringResource
let image: Image
let bodyText: LocalizedStringResource
let buttonTitle: LocalizedStringResource
let buttonAction: () -> Void
let content: Content
init(title: LocalizedStringResource, image: Image, bodyText: LocalizedStringResource, buttonTitle: LocalizedStringResource, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
self.title = title
self.image = image
self.bodyText = bodyText
self.buttonTitle = buttonTitle
self.buttonAction = buttonAction
self.content = content()
}
var body: some View {
VStack {
Text(title)
.font(.title)
Spacer()
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Spacer()
Text(bodyText)
.multilineTextAlignment(.center)
Spacer()
content
Spacer()
Button(buttonTitle) {
buttonAction()
}
}.padding()
}
}
struct SecretAgentSetupView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: .setupAgentTitle,
image: Image(nsImage: NSApplication.shared.applicationIconImage),
bodyText: .setupAgentDescription,
buttonTitle: .setupAgentInstallButton,
buttonAction: install) {
Text(.setupAgentActivityMonitorDescription)
.multilineTextAlignment(.center)
}
}
func install() {
Task {
await LaunchAgentController().install()
buttonAction()
}
}
}
struct SSHAgentSetupView: View {
let buttonAction: () -> Void
private static let controller = ShellConfigurationController()
@State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first!
var body: some View {
SetupStepView(title: .setupSshTitle,
image: Image(systemName: "terminal"),
bodyText: .setupSshDescription,
buttonTitle: .setupSshAddedManuallyButton,
buttonAction: buttonAction) {
Link(.setupThirdPartyFaqLink, destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!)
Picker(selection: $selectedShellInstruction, label: EmptyView()) {
ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in
Text(instruction.shell)
.tag(instruction)
.padding()
}
}.pickerStyle(SegmentedPickerStyle())
CopyableView(title: .setupSshAddToConfigButton(configPath: selectedShellInstruction.shellConfigPath), image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
Button(.setupSshAddForMeButton) {
let controller = ShellConfigurationController()
if controller.addToShell(shellInstructions: selectedShellInstruction) {
buttonAction()
}
}
}
}
}
class Delegate: NSObject, NSOpenSavePanelDelegate {
private let name: String
init(name: String) {
self.name = name
}
func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
return url.lastPathComponent == name
}
}
struct UpdaterExplainerView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: .setupUpdatesTitle,
image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: .setupUpdatesDescription,
buttonTitle: .setupUpdatesOk,
buttonAction: buttonAction) {
Link(.setupUpdatesReadmore, destination: SetupView.Constants.updaterFAQURL)
}
}
}
extension SetupView {
enum Constants {
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
}
}
struct ShellConfigInstruction: Identifiable, Hashable {
var shell: String
var shellConfigDirectory: String
var shellConfigFilename: String
var text: String
var id: String {
shell
}
var shellConfigPath: String {
return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename)
}
}
#if DEBUG
struct SetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SetupView(visible: .constant(true), setupComplete: .constant(false))
}
}
}
struct SecretAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SecretAgentSetupView(buttonAction: {})
}
}
}
struct SSHAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SSHAgentSetupView(buttonAction: {})
}
}
}
struct UpdaterExplainerView_Previews: PreviewProvider {
static var previews: some View {
Group {
UpdaterExplainerView(buttonAction: {})
}
}
}
#endif

View File

@@ -0,0 +1,94 @@
import SwiftUI
struct PrimaryButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
}
}
extension View {
func primaryButton() -> some View {
modifier(PrimaryButtonModifier())
}
}
struct MenuButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
} else {
content
.buttonStyle(.borderless)
}
}
}
extension View {
func menuButton() -> some View {
modifier(MenuButtonModifier())
}
}
struct NormalButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.bordered)
}
}
}
extension View {
func normalButton() -> some View {
modifier(NormalButtonModifier())
}
}
struct DangerButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark {
content.buttonStyle(.glassProminent)
.tint(.red)
.foregroundStyle(.white)
} else {
content.buttonStyle(.borderedProminent)
.tint(.red)
.foregroundStyle(.white)
}
}
}
extension View {
func danger() -> some View {
modifier(DangerButtonModifier())
}
}

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

@@ -0,0 +1,19 @@
import SwiftUI
struct ErrorStyleModifier: ViewModifier {
func body(content: Content) -> some View {
content
.foregroundStyle(.red)
.font(.callout)
}
}
extension View {
func errorStyle() -> some View {
modifier(ErrorStyleModifier())
}
}

View File

@@ -0,0 +1,153 @@
import SwiftUI
struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
if agentStatusChecker.running {
AgentRunningView()
} else {
AgentNotRunningView()
}
}
}
struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
Form {
Section {
if let process = agentStatusChecker.process {
ConfigurationItemView(
title: .agentDetailsLocationTitle,
value: process.bundleURL!.path(),
action: .revealInFinder(process.bundleURL!.path()),
)
ConfigurationItemView(
title: .agentDetailsSocketPathTitle,
value: URL.socketPath,
action: .copy(URL.socketPath),
)
ConfigurationItemView(
title: .agentDetailsVersionTitle,
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
)
if let launchDate = process.launchDate {
ConfigurationItemView(
title: .agentDetailsRunningSinceTitle,
value: launchDate.formatted()
)
}
}
} header: {
Text(.agentRunningNoticeDetailTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentRunningNoticeDetailDescription)
HStack {
Spacer()
Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) {
Task {
_ = await LaunchAgentController()
.uninstall()
agentStatusChecker.check()
}
}
} primaryAction: {
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
}
}
}
}
.padding(.vertical)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
struct AgentNotRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@State var triedRestart = false
@State var loading = false
var body: some View {
Form {
Section {
} header: {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentNotRunningNoticeDetailDescription)
HStack {
if !triedRestart {
Spacer()
Button {
guard !loading else { return }
loading = true
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
loading = false
if !agentStatusChecker.running {
triedRestart = true
}
}
} label: {
if !loading {
Text(.agentDetailsStartAgentButton)
} else {
HStack {
Text(.agentDetailsStartAgentButtonStarting)
ProgressView()
.controlSize(.mini)
}
}
}
.primaryButton()
} else {
Text(.agentDetailsCouldNotStartError)
.bold()
.foregroundStyle(.red)
}
}
}
.padding(.bottom)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
//#Preview {
// AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
//}
//#Preview {
// AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
//}

View File

@@ -36,7 +36,7 @@ struct ContentView: View {
toolbarItem(newItemView, id: "new")
}
.sheet(isPresented: $runningSetup) {
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
SetupView(setupComplete: $hasRunSetup)
}
}
@@ -54,20 +54,12 @@ extension ContentView {
ToolbarItem(id: id) { view }
}
}
var needsSetup: Bool {
(runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild
}
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
/// These two are mutually exclusive
@ViewBuilder
var runningOrRunSetupView: some View {
if needsSetup {
setupNoticeView
} else {
runningNoticeView
}
agentStatusToolbarView
}
var updateNoticeContent: (LocalizedStringResource, Color)? {
@@ -75,7 +67,7 @@ extension ContentView {
if update.critical {
return (.updateCriticalNoticeTitle, .red)
} else {
if updater.testBuild {
if updater.currentVersion.isTestBuild {
return (.updateTestNoticeTitle, .blue)
} else {
return (.updateNormalNoticeTitle, .orange)
@@ -94,8 +86,23 @@ extension ContentView {
.foregroundColor(.white)
})
.buttonStyle(ToolbarButtonStyle(color: color))
.popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in
UpdateDetailView(update: update)
.sheet(item: $selectedUpdate) { update in
VStack {
if updater.currentVersion.isTestBuild {
VStack {
if let description = updater.currentVersion.previewDescription {
Text(description)
}
Link(destination: URL(string: "https://github.com/maxgoedjen/secretive/actions/workflows/nightly.yml")!) {
Button(.updaterDownloadLatestNightlyButton) {}
.frame(maxWidth: .infinity)
.primaryButton()
}
}
.padding()
}
UpdateDetailView(update: update)
}
}
}
}
@@ -103,65 +110,52 @@ extension ContentView {
@ViewBuilder
var newItemView: some View {
if storeList.modifiableStore?.isAvailable ?? false {
Button(action: {
Button(.appMenuNewSecretButton, systemImage: "plus") {
showingCreation = true
}, label: {
Image(systemName: "plus")
})
}
.menuButton()
.sheet(isPresented: $showingCreation) {
if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable, showing: $showingCreation)
.onDisappear {
guard let newest = modifiable.secrets.last else { return }
activeSecret = newest
CreateSecretView(store: modifiable) { created in
if let created {
activeSecret = created
}
}
}
}
}
}
@ViewBuilder
var setupNoticeView: some View {
Button(action: {
runningSetup = true
}, label: {
Group {
if hasRunSetup && !agentStatusChecker.running {
Text(.agentNotRunningNoticeTitle)
} else {
Text(.agentSetupNoticeTitle)
}
}
.font(.headline)
})
.buttonStyle(ToolbarButtonStyle(color: .orange))
}
@ViewBuilder
var runningNoticeView: some View {
var agentStatusToolbarView: some View {
Button(action: {
showingAgentInfo = true
}, label: {
HStack {
Text(.agentRunningNoticeTitle)
.font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.green)
if agentStatusChecker.running {
Text(.agentRunningNoticeTitle)
.font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.green)
} else {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.red)
}
}
})
.buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05)))
.buttonStyle(
ToolbarButtonStyle(
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
)
)
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
VStack {
Text(.agentRunningNoticeDetailTitle)
.font(.title)
.padding(5)
Text(.agentRunningNoticeDetailDescription)
.frame(width: 300)
}
.padding()
AgentStatusView()
}
}
@@ -193,31 +187,22 @@ extension ContentView {
}
var attachmentAnchor: PopoverAttachmentAnchor {
// Ideally .point(.bottom), but broken on Sonoma (FB12726503)
.rect(.bounds)
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
// Empty on modifiable and nonmodifiable
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
.environment(PreviewUpdater())
// 5 items on modifiable and nonmodifiable
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
.environment(PreviewUpdater())
}
}
}
#endif
//#Preview {
// // Empty on modifiable and nonmodifiable
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
// .environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
// .environment(PreviewUpdater())
//}
//
//#Preview {
// // 5 items on modifiable and nonmodifiable
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
// .environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
// .environment(PreviewUpdater())
//}

View File

@@ -6,6 +6,7 @@ struct CopyableView: View {
var title: LocalizedStringResource
var image: Image
var text: String
var showRevealInFinder = false
@State private var interactionState: InteractionState = .normal
@@ -21,9 +22,12 @@ struct CopyableView: View {
.foregroundColor(primaryTextColor)
Spacer()
if interactionState != .normal {
hoverIcon
.bold()
.textCase(.uppercase)
HStack {
if showRevealInFinder {
revealInFinderButton
}
copyButton
}
.foregroundColor(secondaryTextColor)
.transition(.opacity)
}
@@ -72,19 +76,35 @@ struct CopyableView: View {
}
@ViewBuilder
var hoverIcon: some View {
var copyButton: some View {
switch interactionState {
case .hovering:
Image(systemName: "document.on.document")
.accessibilityLabel(String(localized: "copyable_click_to_copy_button"))
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
withAnimation {
// Button will eat the click, so we set interaction state manually.
interactionState = .clicking
}
copy()
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case .clicking:
Image(systemName: "checkmark.circle.fill")
.accessibilityLabel(String(localized: "copyable_copied"))
.accessibilityLabel(String(localized: .copyableCopied))
case .normal, .dragging:
EmptyView()
}
}
var revealInFinderButton: some View {
Button(.revealInFinderButton, systemImage: "folder") {
let (processedPath, folder) = text.normalizedPathAndFolder
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
}
var primaryTextColor: Color {
switch interactionState {
case .normal, .hovering, .dragging:
@@ -163,17 +183,12 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
}
#if DEBUG
struct CopyableView_Previews: PreviewProvider {
static var previews: some View {
Group {
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding()
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding()
}
}
#Preview {
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding()
}
#endif
#Preview {
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding()
}

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,31 @@
<?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>ApplicationDescription</key>
<string>Secretive is an app for storing and managing SSH keys in the Secure Enclave</string>
<key>DeveloperName</key>
<string>Max Goedjen</string>
<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>
<key>Services</key>
<array/>
</dict>
</plist>

View File

@@ -0,0 +1,17 @@
import Foundation
import OSLog
import XPCWrappers
import Brief
final class SecretiveUpdater: NSObject, XPCProtocol {
enum Constants {
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases")!
}
func process(_: Data) async throws -> [Release] {
let (data, _) = try await URLSession.shared.data(from: Constants.updateURL)
return try JSONDecoder().decode([Release].self, from: data)
}
}

View File

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