mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-09 18:57:22 +02:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d16a6ffb1 | ||
|
|
95658072e7 | ||
|
|
7eeb09b1ec | ||
|
|
e6f21c13b0 | ||
|
|
96ef91df0c | ||
|
|
aa46d8fa48 | ||
|
|
cf7c6e9fbe | ||
|
|
7c7db56c1e | ||
|
|
cd12e4c828 | ||
|
|
a5b43ea046 | ||
|
|
8c516e128a | ||
|
|
6854c05763 | ||
|
|
5467474d88 | ||
|
|
5c2d039682 | ||
|
|
20e64604d6 | ||
|
|
c5a610d786 | ||
|
|
7d21e3983c | ||
|
|
2c38aaed6f | ||
|
|
63b42bd9df | ||
|
|
cf5ae49ebc | ||
|
|
61705af42f | ||
|
|
558ae15b2d | ||
|
|
902d5c4a1e | ||
|
|
e0c24917f2 | ||
|
|
3d5f0b45bd | ||
|
|
74f4f1c0b1 |
47
.github/workflows/codeql.yml
vendored
Normal file
47
.github/workflows/codeql.yml
vendored
Normal 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}}"
|
||||
6
.github/workflows/nightly.yml
vendored
6
.github/workflows/nightly.yml
vendored
@@ -7,8 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# runs-on: macOS-latest
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
@@ -31,7 +30,8 @@ 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
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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,6 +75,10 @@ let package = Package(
|
||||
name: "BriefTests",
|
||||
dependencies: ["Brief"],
|
||||
),
|
||||
.target(
|
||||
name: "XPCWrappers",
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -89,5 +90,6 @@ var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||
[
|
||||
.swiftLanguageMode(.v6),
|
||||
.treatAllWarnings(as: .error),
|
||||
.strictMemorySafety()
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -31,46 +31,29 @@ public final class Agent: Sendable {
|
||||
|
||||
extension Agent {
|
||||
|
||||
/// Handles an incoming request.
|
||||
/// - Parameters:
|
||||
/// - data: The data to handle.
|
||||
/// - provenance: The origin of the request.
|
||||
/// - Returns: A response data payload.
|
||||
public func handle(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
logger.debug("Agent handling new data")
|
||||
guard data.count > 4 else {
|
||||
throw InvalidDataProvidedError()
|
||||
}
|
||||
let requestTypeInt = data[4]
|
||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||
return SSHAgent.ResponseType.agentFailure.data.lengthAndData
|
||||
}
|
||||
logger.debug("Agent handling request of type \(requestType.debugDescription)")
|
||||
let subData = Data(data[5...])
|
||||
let response = await handle(requestType: requestType, data: subData, provenance: provenance)
|
||||
return response
|
||||
}
|
||||
|
||||
private func handle(requestType: SSHAgent.RequestType, data: Data, provenance: SigningRequestProvenance) async -> Data {
|
||||
public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data {
|
||||
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
|
||||
await reloadSecretsIfNeccessary()
|
||||
var response = Data()
|
||||
do {
|
||||
switch requestType {
|
||||
switch request {
|
||||
case .requestIdentities:
|
||||
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
||||
response.append(SSHAgent.Response.agentIdentitiesAnswer.data)
|
||||
response.append(await identities())
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
|
||||
case .signRequest:
|
||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||
response.append(try await sign(data: data, provenance: provenance))
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
||||
logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)")
|
||||
case .signRequest(let context):
|
||||
response.append(SSHAgent.Response.agentSignResponse.data)
|
||||
response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance))
|
||||
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
|
||||
case .unknown(let value):
|
||||
logger.error("Agent received unknown request of type \(value).")
|
||||
default:
|
||||
logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.")
|
||||
throw UnhandledRequestError()
|
||||
}
|
||||
} catch {
|
||||
response.removeAll()
|
||||
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||
response = SSHAgent.Response.agentFailure.data
|
||||
logger.debug("Agent returned \(SSHAgent.Response.agentFailure.debugDescription)")
|
||||
}
|
||||
return response.lengthAndData
|
||||
}
|
||||
@@ -101,7 +84,7 @@ extension Agent {
|
||||
}
|
||||
logger.log("Agent enumerated \(count) identities")
|
||||
var countBigEndian = UInt32(count).bigEndian
|
||||
let countData = Data(bytes: &countBigEndian, count: UInt32.bitWidth/8)
|
||||
let countData = unsafe Data(bytes: &countBigEndian, count: MemoryLayout<UInt32>.size)
|
||||
return countData + keyData
|
||||
}
|
||||
|
||||
@@ -110,27 +93,16 @@ extension Agent {
|
||||
/// - data: The data to sign.
|
||||
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
|
||||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
let reader = OpenSSHReader(data: data)
|
||||
let payloadHash = reader.readNextChunk()
|
||||
let hash: Data
|
||||
|
||||
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
|
||||
if let certificatePublicKey = await certificateHandler.publicKeyHash(from: payloadHash) {
|
||||
hash = certificatePublicKey
|
||||
} else {
|
||||
hash = payloadHash
|
||||
}
|
||||
|
||||
guard let (secret, store) = await secret(matching: hash) else {
|
||||
logger.debug("Agent did not have a key matching \(hash as NSData)")
|
||||
func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
guard let (secret, store) = await secret(matching: keyBlob) else {
|
||||
let keyBlobHex = keyBlob.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }.joined()
|
||||
logger.debug("Agent did not have a key matching \(keyBlobHex)")
|
||||
throw NoMatchingKeyError()
|
||||
}
|
||||
|
||||
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
||||
|
||||
let dataToSign = reader.readNextChunk()
|
||||
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
|
||||
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance)
|
||||
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
||||
|
||||
try await witness?.witness(accessTo: secret, from: store, by: provenance)
|
||||
@@ -169,16 +141,16 @@ extension Agent {
|
||||
|
||||
extension Agent {
|
||||
|
||||
struct InvalidDataProvidedError: Error {}
|
||||
struct NoMatchingKeyError: Error {}
|
||||
struct UnhandledRequestError: Error {}
|
||||
|
||||
}
|
||||
|
||||
extension SSHAgent.ResponseType {
|
||||
extension SSHAgent.Response {
|
||||
|
||||
var data: Data {
|
||||
var raw = self.rawValue
|
||||
return Data(bytes: &raw, count: UInt8.bitWidth/8)
|
||||
return unsafe Data(bytes: &raw, count: MemoryLayout<UInt8>.size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SecretKit
|
||||
|
||||
/// Manages storage and lookup for OpenSSH certificates.
|
||||
public actor OpenSSHCertificateHandler: Sendable {
|
||||
@@ -25,29 +26,6 @@ public actor OpenSSHCertificateHandler: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstructs a public key from a ``Data``, if that ``Data`` contains an OpenSSH certificate hash. Currently only ecdsa certificates are supported
|
||||
/// - Parameter certBlock: The openssh certificate to extract the public key from
|
||||
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
|
||||
public func publicKeyHash(from hash: Data) -> Data? {
|
||||
let reader = OpenSSHReader(data: hash)
|
||||
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
|
||||
switch certType {
|
||||
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
||||
_ = reader.readNextChunk() // nonce
|
||||
let curveIdentifier = reader.readNextChunk()
|
||||
let publicKey = reader.readNextChunk()
|
||||
|
||||
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
return openSSHIdentifier.lengthAndData +
|
||||
curveIdentifier.lengthAndData +
|
||||
publicKey.lengthAndData
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
||||
/// - Parameter secret: The secret to search for a certificate with
|
||||
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
||||
47
Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift
Normal file
47
Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -133,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)!
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
module SecretAgentKitHeaders [system] {
|
||||
header "include/SecretAgentKit.h"
|
||||
export *
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ extension OpenSSHPublicKeyWriter {
|
||||
|
||||
extension OpenSSHPublicKeyWriter {
|
||||
|
||||
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
|
||||
func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
|
||||
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
|
||||
// Keychain stores it as a thin ASN.1 wrapper with this format:
|
||||
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,7 +27,7 @@ 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 migratedAny = false
|
||||
@@ -40,7 +40,7 @@ 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
import Observation
|
||||
import Security
|
||||
import CryptoKit
|
||||
@preconcurrency import LocalAuthentication
|
||||
import LocalAuthentication
|
||||
import SecretKit
|
||||
import os
|
||||
|
||||
@@ -40,7 +40,7 @@ extension SecureEnclave {
|
||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
||||
var context: LAContext
|
||||
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
|
||||
context = existing.context
|
||||
context = unsafe existing.context
|
||||
} else {
|
||||
let newContext = LAContext()
|
||||
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
|
||||
@@ -57,7 +57,7 @@ extension SecureEnclave {
|
||||
kSecReturnData: true,
|
||||
])
|
||||
var untyped: CFTypeRef?
|
||||
let status = SecItemCopyMatching(queryAttributes, &untyped)
|
||||
let status = unsafe SecItemCopyMatching(queryAttributes, &untyped)
|
||||
if status != errSecSuccess {
|
||||
throw KeychainError(statusCode: status)
|
||||
}
|
||||
@@ -121,12 +121,12 @@ extension SecureEnclave {
|
||||
fatalError()
|
||||
}
|
||||
let access =
|
||||
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
||||
unsafe SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||
flags,
|
||||
&accessError)
|
||||
if let error = accessError {
|
||||
throw error.takeRetainedValue() as Error
|
||||
if let error = unsafe accessError {
|
||||
throw unsafe error.takeRetainedValue() as Error
|
||||
}
|
||||
let dataRep: Data
|
||||
let publicKey: Data
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
14
Sources/Packages/Sources/XPCWrappers/XPCProtocol.swift
Normal file
14
Sources/Packages/Sources/XPCWrappers/XPCProtocol.swift
Normal 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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
53
Sources/Packages/Sources/XPCWrappers/XPCTypedSession.swift
Normal file
53
Sources/Packages/Sources/XPCWrappers/XPCTypedSession.swift
Normal 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 {}
|
||||
|
||||
}
|
||||
|
||||
@@ -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=")!
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
23
Sources/SecretAgent/XPCInputParser.swift
Normal file
23
Sources/SecretAgent/XPCInputParser.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
11
Sources/SecretAgentInputParser/Info.plist
Normal file
11
Sources/SecretAgentInputParser/Info.plist
Normal 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>
|
||||
17
Sources/SecretAgentInputParser/SecretAgentInputParser.swift
Normal file
17
Sources/SecretAgentInputParser/SecretAgentInputParser.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
7
Sources/SecretAgentInputParser/main.swift
Normal file
7
Sources/SecretAgentInputParser/main.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
import XPCWrappers
|
||||
|
||||
let delegate = XPCServiceDelegate(exportedObject: SecretAgentInputParser())
|
||||
let listener = NSXPCListener.service()
|
||||
listener.delegate = delegate
|
||||
listener.resume()
|
||||
@@ -25,11 +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 */; };
|
||||
@@ -42,6 +44,17 @@
|
||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
||||
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
|
||||
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
50692D282E6FDB8D0043C7BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692D242E6FDB8D0043C7BB /* main.swift */; };
|
||||
50692D2D2E6FDC000043C7BB /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 50692D2C2E6FDC000043C7BB /* XPCWrappers */; };
|
||||
50692D2F2E6FDC2B0043C7BB /* SecretiveUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692D2E2E6FDC290043C7BB /* SecretiveUpdater.swift */; };
|
||||
50692D312E6FDC390043C7BB /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 50692D302E6FDC390043C7BB /* Brief */; };
|
||||
50692E5B2E6FF9D20043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
50692E682E6FF9E20043C7BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692E632E6FF9E20043C7BB /* main.swift */; };
|
||||
50692E692E6FF9E20043C7BB /* SecretAgentInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50692E642E6FF9E20043C7BB /* SecretAgentInputParser.swift */; };
|
||||
50692E6C2E6FFA510043C7BB /* SecretAgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 50692E6B2E6FFA510043C7BB /* SecretAgentKit */; };
|
||||
50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
|
||||
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */; };
|
||||
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; };
|
||||
@@ -70,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;
|
||||
@@ -113,11 +185,13 @@
|
||||
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; };
|
||||
@@ -133,6 +207,15 @@
|
||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
|
||||
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
|
||||
50692BA52E6D5CC90043C7BB /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
|
||||
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveUpdater.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
50692D232E6FDB8D0043C7BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50692D242E6FDB8D0043C7BB /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
50692D2E2E6FDC290043C7BB /* SecretiveUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretiveUpdater.swift; sourceTree = "<group>"; };
|
||||
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretAgentInputParser.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
50692E622E6FF9E20043C7BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50692E632E6FF9E20043C7BB /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
50692E642E6FF9E20043C7BB /* SecretAgentInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretAgentInputParser.swift; sourceTree = "<group>"; };
|
||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
|
||||
508A58A9241E06B40069DC07 /* PreviewUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewUpdater.swift; sourceTree = "<group>"; };
|
||||
508A58AB241E121B0069DC07 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -170,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;
|
||||
@@ -198,6 +298,7 @@
|
||||
children = (
|
||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
||||
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
|
||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
||||
);
|
||||
path = Styles;
|
||||
@@ -249,6 +350,8 @@
|
||||
50617D8123FCE48E0099B055 /* Secretive */,
|
||||
50A3B78B24026B7500D209EA /* SecretAgent */,
|
||||
508A58AF241E144C0069DC07 /* Config */,
|
||||
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */,
|
||||
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */,
|
||||
50617D8023FCE48E0099B055 /* Products */,
|
||||
5099A08B240243730062B6F2 /* Frameworks */,
|
||||
);
|
||||
@@ -259,6 +362,8 @@
|
||||
children = (
|
||||
50617D7F23FCE48E0099B055 /* Secretive.app */,
|
||||
50A3B78A24026B7500D209EA /* SecretAgent.app */,
|
||||
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */,
|
||||
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -292,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 = (
|
||||
@@ -336,6 +462,7 @@
|
||||
children = (
|
||||
50020BAF24064869003D4025 /* AppDelegate.swift */,
|
||||
5018F54E24064786002EB505 /* Notifier.swift */,
|
||||
501578122E6C0479004A37D0 /* XPCInputParser.swift */,
|
||||
50A3B79524026B7600D209EA /* Main.storyboard */,
|
||||
50A3B79824026B7600D209EA /* Info.plist */,
|
||||
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
|
||||
@@ -365,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 = (
|
||||
@@ -382,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" */;
|
||||
@@ -390,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 = (
|
||||
@@ -414,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;
|
||||
};
|
||||
@@ -450,6 +631,8 @@
|
||||
targets = (
|
||||
50617D7E23FCE48D0099B055 /* Secretive */,
|
||||
50A3B78924026B7500D209EA /* SecretAgent */,
|
||||
50692D112E6FDB880043C7BB /* SecretiveUpdater */,
|
||||
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -467,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;
|
||||
@@ -491,6 +688,7 @@
|
||||
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 */,
|
||||
@@ -520,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;
|
||||
};
|
||||
@@ -537,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 */
|
||||
@@ -621,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;
|
||||
@@ -688,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;
|
||||
@@ -708,7 +953,7 @@
|
||||
ENABLE_ENHANCED_SECURITY = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
@@ -748,7 +993,7 @@
|
||||
ENABLE_ENHANCED_SECURITY = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
@@ -772,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 */;
|
||||
@@ -842,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;
|
||||
@@ -860,7 +1326,7 @@
|
||||
ENABLE_ENHANCED_SECURITY = YES;
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
@@ -894,7 +1360,7 @@
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
@@ -929,7 +1395,7 @@
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
@@ -965,7 +1431,7 @@
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
@@ -1011,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 = (
|
||||
@@ -1060,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 */;
|
||||
|
||||
@@ -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>
|
||||
@@ -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,8 +36,8 @@ 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
|
||||
@@ -52,7 +55,7 @@ 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 {
|
||||
@@ -89,7 +92,6 @@ struct Secretive: App {
|
||||
extension Secretive {
|
||||
|
||||
private func reinstallAgent() {
|
||||
justUpdatedChecker.check()
|
||||
Task {
|
||||
_ = await LaunchAgentController().install()
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,4 +9,17 @@ extension URL {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -40,10 +40,7 @@ struct ConfigurationItemView<Content: View>: View {
|
||||
.buttonStyle(.borderless)
|
||||
case .revealInFinder(let rawPath):
|
||||
Button(.revealInFinderButton, systemImage: "folder") {
|
||||
// All foundation-based normalization methods replace this with the container directly.
|
||||
let processedPath = rawPath.replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
|
||||
let url = URL(filePath: processedPath)
|
||||
let folder = url.deletingLastPathComponent().path()
|
||||
let (processedPath, folder) = rawPath.normalizedPathAndFolder
|
||||
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
|
||||
@@ -21,9 +21,10 @@ struct SetupView: View {
|
||||
StepView(
|
||||
title: .setupAgentTitle,
|
||||
description: .setupAgentDescription,
|
||||
detail: .setupAgentActivityMonitorDescription,
|
||||
systemImage: "lock.laptopcomputer",
|
||||
) {
|
||||
setupButton(
|
||||
SetupButton(
|
||||
.setupAgentInstallButton,
|
||||
complete: installed,
|
||||
width: buttonWidth
|
||||
@@ -40,7 +41,7 @@ struct SetupView: View {
|
||||
description: .setupUpdatesDescription,
|
||||
systemImage: "network.badge.shield.half.filled",
|
||||
) {
|
||||
setupButton(
|
||||
SetupButton(
|
||||
.setupUpdatesOkButton,
|
||||
complete: updates,
|
||||
width: buttonWidth
|
||||
@@ -54,7 +55,7 @@ struct SetupView: View {
|
||||
description: .setupIntegrationsDescription,
|
||||
systemImage: "firewall",
|
||||
) {
|
||||
setupButton(
|
||||
SetupButton(
|
||||
.setupIntegrationsButton,
|
||||
complete: integrations,
|
||||
width: buttonWidth
|
||||
@@ -63,7 +64,7 @@ struct SetupView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onPreferenceChange(setupButton.WidthKey.self) { width in
|
||||
.onPreferenceChange(SetupButton.WidthKey.self) { width in
|
||||
buttonWidth = width
|
||||
}
|
||||
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||
@@ -88,7 +89,7 @@ struct SetupView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct setupButton: View {
|
||||
struct SetupButton: View {
|
||||
|
||||
struct WidthKey: @MainActor PreferenceKey {
|
||||
@MainActor static var defaultValue: CGFloat? = nil
|
||||
@@ -144,12 +145,20 @@ struct StepView<Content: View>: View {
|
||||
let title: LocalizedStringResource
|
||||
let icon: Image
|
||||
let description: LocalizedStringResource
|
||||
let detail: LocalizedStringResource?
|
||||
let actions: Content
|
||||
|
||||
init(title: LocalizedStringResource, description: LocalizedStringResource, systemImage: String, actions: () -> Content) {
|
||||
init(
|
||||
title: LocalizedStringResource,
|
||||
description: LocalizedStringResource,
|
||||
detail: LocalizedStringResource? = nil,
|
||||
systemImage: String,
|
||||
actions: () -> Content
|
||||
) {
|
||||
self.title = title
|
||||
self.icon = Image(systemName: systemImage)
|
||||
self.description = description
|
||||
self.detail = detail
|
||||
self.actions = actions()
|
||||
}
|
||||
|
||||
@@ -165,6 +174,11 @@ struct StepView<Content: View>: View {
|
||||
Text(title)
|
||||
.bold()
|
||||
Text(description)
|
||||
if let detail {
|
||||
Text(detail)
|
||||
.font(.callout)
|
||||
.italic()
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 20)
|
||||
actions
|
||||
|
||||
@@ -28,7 +28,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
Section {
|
||||
TextField(String(localized: .createSecretNameLabel), text: $name, prompt: Text(.createSecretNamePlaceholder))
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Picker(.createSecretRequireAuthenticationTitle, selection: $authenticationRequirement) {
|
||||
Picker(.createSecretProtectionLevelTitle, selection: $authenticationRequirement) {
|
||||
ForEach(authenticationOptions) { option in
|
||||
HStack {
|
||||
switch option {
|
||||
@@ -66,7 +66,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
Text(.createSecretBiometryCurrentWarning)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 3)
|
||||
.background(.red.opacity(0.5), in: RoundedRectangle(cornerRadius: 5))
|
||||
.boxBackground(color: .red)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -85,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) {
|
||||
|
||||
@@ -5,16 +5,16 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
|
||||
let store: StoreType
|
||||
let secret: StoreType.SecretType
|
||||
let dismissalBlock: (_ renamed: Bool) -> ()
|
||||
|
||||
@State private var name: String
|
||||
@State private var publicKeyAttribution: String
|
||||
@State var errorText: String?
|
||||
|
||||
init(store: StoreType, secret: StoreType.SecretType, dismissalBlock: @escaping (Bool) -> ()) {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
init(store: StoreType, secret: StoreType.SecretType) {
|
||||
self.store = store
|
||||
self.secret = secret
|
||||
self.dismissalBlock = dismissalBlock
|
||||
name = secret.name
|
||||
publicKeyAttribution = secret.publicKeyAttribution ?? ""
|
||||
}
|
||||
@@ -39,7 +39,7 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
}
|
||||
HStack {
|
||||
Button(.editCancelButton) {
|
||||
dismissalBlock(false)
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
Button(.editSaveButton, action: rename)
|
||||
@@ -58,7 +58,7 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
Task {
|
||||
do {
|
||||
try await store.update(secret: secret, name: name, attributes: attributes)
|
||||
dismissalBlock(true)
|
||||
dismiss()
|
||||
} catch {
|
||||
errorText = error.localizedDescription
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ struct EmptyStoreImmutableView: View {
|
||||
}
|
||||
|
||||
struct EmptyStoreModifiableView: View {
|
||||
|
||||
|
||||
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { windowGeometry in
|
||||
VStack {
|
||||
@@ -51,6 +53,21 @@ 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)
|
||||
}
|
||||
@@ -61,6 +78,10 @@ struct EmptyStoreModifiableView: View {
|
||||
#Preview {
|
||||
EmptyStoreImmutableView()
|
||||
}
|
||||
#Preview {
|
||||
EmptyStoreImmutableView()
|
||||
// .environment(\.justUpdatedChecker, <#T##value: V##V#>)
|
||||
}
|
||||
#Preview {
|
||||
EmptyStoreModifiableView()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
32
Sources/Secretive/Views/Styles/BoxBackgroundStyle.swift
Normal file
32
Sources/Secretive/Views/Styles/BoxBackgroundStyle.swift
Normal 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()
|
||||
}
|
||||
@@ -54,20 +54,12 @@ extension ContentView {
|
||||
ToolbarItem(id: id) { view }
|
||||
}
|
||||
}
|
||||
|
||||
var needsSetup: Bool {
|
||||
runningSetup || !hasRunSetup
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
agentStatusToolbarView
|
||||
}
|
||||
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)
|
||||
@@ -95,7 +87,22 @@ extension ContentView {
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: color))
|
||||
.sheet(item: $selectedUpdate) { update in
|
||||
UpdateDetailView(update: update)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,19 +126,6 @@ extension ContentView {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var setupNoticeView: some View {
|
||||
Button(action: {
|
||||
runningSetup = true
|
||||
}, label: {
|
||||
if !hasRunSetup {
|
||||
Text(.agentSetupNoticeTitle)
|
||||
.font(.headline)
|
||||
}
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var agentStatusToolbarView: some View {
|
||||
Button(action: {
|
||||
|
||||
@@ -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,11 +76,18 @@ struct CopyableView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var hoverIcon: some View {
|
||||
var copyButton: some View {
|
||||
switch interactionState {
|
||||
case .hovering:
|
||||
Image(systemName: "document.on.document")
|
||||
.accessibilityLabel(String(localized: .copyableClickToCopyButton))
|
||||
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: .copyableCopied))
|
||||
@@ -85,6 +96,15 @@ struct CopyableView: View {
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
11
Sources/SecretiveUpdater/Info.plist
Normal file
11
Sources/SecretiveUpdater/Info.plist
Normal 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>
|
||||
31
Sources/SecretiveUpdater/InternetAccessPolicy.plist
Normal file
31
Sources/SecretiveUpdater/InternetAccessPolicy.plist
Normal 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>
|
||||
17
Sources/SecretiveUpdater/SecretiveUpdater.swift
Normal file
17
Sources/SecretiveUpdater/SecretiveUpdater.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
7
Sources/SecretiveUpdater/main.swift
Normal file
7
Sources/SecretiveUpdater/main.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
import XPCWrappers
|
||||
|
||||
let delegate = XPCServiceDelegate(exportedObject: SecretiveUpdater())
|
||||
let listener = NSXPCListener.service()
|
||||
listener.delegate = delegate
|
||||
listener.resume()
|
||||
Reference in New Issue
Block a user