mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-06-21 13:41:36 +00:00
Compare commits
12 Commits
sshextensi
...
socket_lau
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
688b6380bd | ||
|
|
702e3f2cb0 | ||
|
|
76baba746c | ||
|
|
18107257ba | ||
|
|
98e2f38e46 | ||
|
|
a727d110c8 | ||
|
|
fbc4133f39 | ||
|
|
437386b87e | ||
|
|
9bdf9775d2 | ||
|
|
03a31fb474 | ||
|
|
b337b24641 | ||
|
|
2f4d10d70d |
5
.github/workflows/nightly.yml
vendored
5
.github/workflows/nightly.yml
vendored
@@ -11,6 +11,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
attestations: write
|
attestations: write
|
||||||
|
artifact-metadata: write
|
||||||
actions: read
|
actions: read
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
@@ -40,7 +41,7 @@ jobs:
|
|||||||
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
||||||
- name: Upload App to Artifacts
|
- name: Upload App to Artifacts
|
||||||
id: upload
|
id: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Secretive
|
name: Secretive
|
||||||
path: Artifact
|
path: Artifact
|
||||||
@@ -59,7 +60,7 @@ jobs:
|
|||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
- name: Attest
|
- name: Attest
|
||||||
id: attest
|
id: attest
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4
|
||||||
with:
|
with:
|
||||||
subject-name: "Secretive.zip"
|
subject-name: "Secretive.zip"
|
||||||
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
||||||
|
|||||||
5
.github/workflows/oneoff.yml
vendored
5
.github/workflows/oneoff.yml
vendored
@@ -10,6 +10,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
attestations: write
|
attestations: write
|
||||||
|
artifact-metadata: write
|
||||||
actions: read
|
actions: read
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
@@ -39,7 +40,7 @@ jobs:
|
|||||||
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
||||||
- name: Upload App to Artifacts
|
- name: Upload App to Artifacts
|
||||||
id: upload
|
id: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Secretive
|
name: Secretive
|
||||||
path: Artifact
|
path: Artifact
|
||||||
@@ -58,7 +59,7 @@ jobs:
|
|||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
- name: Attest
|
- name: Attest
|
||||||
id: attest
|
id: attest
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4
|
||||||
with:
|
with:
|
||||||
subject-name: "Secretive.zip"
|
subject-name: "Secretive.zip"
|
||||||
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
||||||
|
|||||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -32,6 +32,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
attestations: write
|
attestations: write
|
||||||
|
artifact-metadata: write
|
||||||
actions: read
|
actions: read
|
||||||
runs-on: macos-26
|
runs-on: macos-26
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
@@ -63,7 +64,7 @@ jobs:
|
|||||||
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
||||||
- name: Upload App to Artifacts
|
- name: Upload App to Artifacts
|
||||||
id: upload
|
id: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Secretive.zip
|
name: Secretive.zip
|
||||||
path: Artifact
|
path: Artifact
|
||||||
@@ -82,7 +83,7 @@ jobs:
|
|||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
- name: Attest
|
- name: Attest
|
||||||
id: attest
|
id: attest
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4
|
||||||
with:
|
with:
|
||||||
subject-path: "Secretive.zip"
|
subject-path: "Secretive.zip"
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ Security is obviously paramount for a project like Secretive. As such, any contr
|
|||||||
|
|
||||||
Secretive is designed to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected.
|
Secretive is designed to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected.
|
||||||
|
|
||||||
|
### AI/LLM Policy
|
||||||
|
|
||||||
|
For security and auditing reasons similar to the policy Secretive has on dependencies, any code generated with AI or LLM tools will not be accepted.
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md)
|
All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
|
|||||||
@@ -22,9 +22,15 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "SmartCardSecretKit",
|
name: "SmartCardSecretKit",
|
||||||
targets: ["SmartCardSecretKit"]),
|
targets: ["SmartCardSecretKit"]),
|
||||||
|
.library(
|
||||||
|
name: "CertificateKit",
|
||||||
|
targets: ["CertificateKit"]),
|
||||||
.library(
|
.library(
|
||||||
name: "SSHProtocolKit",
|
name: "SSHProtocolKit",
|
||||||
targets: ["SSHProtocolKit"]),
|
targets: ["SSHProtocolKit"]),
|
||||||
|
.library(
|
||||||
|
name: "Formatters",
|
||||||
|
targets: ["Formatters"]),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
],
|
],
|
||||||
@@ -56,9 +62,16 @@ let package = Package(
|
|||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "CertificateKit",
|
||||||
|
dependencies: ["SecretKit", "Formatters"],
|
||||||
|
path: "Sources/Packages/Sources/CertificateKit",
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings,
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SSHProtocolKit",
|
name: "SSHProtocolKit",
|
||||||
dependencies: ["SecretKit"],
|
dependencies: ["SecretKit", "CertificateKit"],
|
||||||
path: "Sources/Packages/Sources/SSHProtocolKit",
|
path: "Sources/Packages/Sources/SSHProtocolKit",
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
@@ -69,6 +82,13 @@ let package = Package(
|
|||||||
path: "Sources/Packages/Tests/SSHProtocolKitTests",
|
path: "Sources/Packages/Tests/SSHProtocolKitTests",
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "Formatters",
|
||||||
|
dependencies: [],
|
||||||
|
path: "Sources/Packages/Sources/Formatters",
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,21 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "SmartCardSecretKit",
|
name: "SmartCardSecretKit",
|
||||||
targets: ["SmartCardSecretKit"]),
|
targets: ["SmartCardSecretKit"]),
|
||||||
|
.library(
|
||||||
|
name: "CertificateKit",
|
||||||
|
targets: ["CertificateKit"]),
|
||||||
.library(
|
.library(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
targets: ["SecretAgentKit"]),
|
targets: ["SecretAgentKit"]),
|
||||||
|
.library(
|
||||||
|
name: "Formatters",
|
||||||
|
targets: ["Formatters"]),
|
||||||
.library(
|
.library(
|
||||||
name: "Common",
|
name: "Common",
|
||||||
targets: ["Common"]),
|
targets: ["Common"]),
|
||||||
|
.library(
|
||||||
|
name: "SharedXPCServices",
|
||||||
|
targets: ["SharedXPCServices"]),
|
||||||
.library(
|
.library(
|
||||||
name: "Brief",
|
name: "Brief",
|
||||||
targets: ["Brief"]),
|
targets: ["Brief"]),
|
||||||
@@ -61,9 +70,15 @@ let package = Package(
|
|||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "CertificateKit",
|
||||||
|
dependencies: ["SecretKit", "Formatters"],
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings,
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
dependencies: ["SecretKit", "SSHProtocolKit", "Common"],
|
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
@@ -73,7 +88,7 @@ let package = Package(
|
|||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SSHProtocolKit",
|
name: "SSHProtocolKit",
|
||||||
dependencies: ["SecretKit"],
|
dependencies: ["SecretKit", "CertificateKit"],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
@@ -82,12 +97,24 @@ let package = Package(
|
|||||||
dependencies: ["SSHProtocolKit"],
|
dependencies: ["SSHProtocolKit"],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "Formatters",
|
||||||
|
dependencies: [],
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings,
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "Common",
|
name: "Common",
|
||||||
dependencies: ["SSHProtocolKit", "SecretKit"],
|
dependencies: ["SSHProtocolKit", "SecretKit"],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "SharedXPCServices",
|
||||||
|
dependencies: ["CertificateKit", "SSHProtocolKit"],
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings,
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "Brief",
|
name: "Brief",
|
||||||
dependencies: ["XPCWrappers", "SSHProtocolKit"],
|
dependencies: ["XPCWrappers", "SSHProtocolKit"],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -36,11 +36,11 @@ import XPCWrappers
|
|||||||
self.currentVersion = currentVersion
|
self.currentVersion = currentVersion
|
||||||
Task {
|
Task {
|
||||||
if checkOnLaunch {
|
if checkOnLaunch {
|
||||||
try await checkForUpdates()
|
try? await checkForUpdates()
|
||||||
}
|
}
|
||||||
while !Task.isCancelled {
|
while !Task.isCancelled {
|
||||||
try? await Task.sleep(for: .seconds(Int(checkFrequency)))
|
try? await Task.sleep(for: .seconds(Int(checkFrequency)))
|
||||||
try await checkForUpdates()
|
try? await checkForUpdates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
Sources/Packages/Sources/CertificateKit/Certificate.swift
Normal file
24
Sources/Packages/Sources/CertificateKit/Certificate.swift
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
import Formatters
|
||||||
|
|
||||||
|
@dynamicMemberLookup
|
||||||
|
public struct Certificate: Sendable, Codable, Equatable, Hashable, Identifiable, CustomDebugStringConvertible {
|
||||||
|
|
||||||
|
public var openSSHCertificate: OpenSSHCertificate
|
||||||
|
public let rawData: Data
|
||||||
|
|
||||||
|
public init(openSSHCertificate: OpenSSHCertificate, rawData: Data) {
|
||||||
|
self.openSSHCertificate = openSSHCertificate
|
||||||
|
self.rawData = rawData
|
||||||
|
}
|
||||||
|
|
||||||
|
public var id: String { Insecure.MD5.hash(data: rawData).formatted(.hex(separator: "")) }
|
||||||
|
|
||||||
|
public var debugDescription: String { openSSHCertificate.debugDescription }
|
||||||
|
|
||||||
|
public subscript<T>(dynamicMember keyPath: KeyPath<OpenSSHCertificate, T>) -> T {
|
||||||
|
openSSHCertificate[keyPath: keyPath]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
153
Sources/Packages/Sources/CertificateKit/CertificateStore.swift
Normal file
153
Sources/Packages/Sources/CertificateKit/CertificateStore.swift
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
import Security
|
||||||
|
import os
|
||||||
|
import SecretKit
|
||||||
|
|
||||||
|
@Observable @MainActor public final class CertificateStore: Sendable {
|
||||||
|
|
||||||
|
public private(set) var certificates: [Certificate] = []
|
||||||
|
|
||||||
|
/// Initializes a Store.
|
||||||
|
public init() {
|
||||||
|
loadCertificates()
|
||||||
|
Task {
|
||||||
|
for await note in DistributedNotificationCenter.default().notifications(named: .certificateStoreUpdated) {
|
||||||
|
guard Constants.notificationToken != (note.object as? String) else {
|
||||||
|
// Don't reload if we're the ones triggering this by reloading.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
loadCertificates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func reloadCertificates() {
|
||||||
|
let before = certificates
|
||||||
|
certificates.removeAll()
|
||||||
|
loadCertificates()
|
||||||
|
if certificates != before {
|
||||||
|
NotificationCenter.default.post(name: .certificateStoreReloaded, object: self)
|
||||||
|
DistributedNotificationCenter.default().postNotificationName(.certificateStoreUpdated, object: Constants.notificationToken, deliverImmediately: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func save(certificate: Certificate) throws {
|
||||||
|
let attributes = try JSONEncoder().encode(certificate.openSSHCertificate)
|
||||||
|
let keychainAttributes = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecAttrAccount: certificate.id,
|
||||||
|
kSecUseDataProtectionKeychain: true,
|
||||||
|
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
|
kSecValueData: certificate.rawData,
|
||||||
|
kSecAttrGeneric: attributes
|
||||||
|
])
|
||||||
|
let status = SecItemAdd(keychainAttributes, nil)
|
||||||
|
if status != errSecSuccess && status != errSecDuplicateItem {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
reloadCertificates()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func delete(certificate: Certificate) throws {
|
||||||
|
let deleteAttributes = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecUseDataProtectionKeychain: true,
|
||||||
|
kSecAttrAccount: certificate.id,
|
||||||
|
])
|
||||||
|
let status = SecItemDelete(deleteAttributes)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
reloadCertificates()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(certificate: Certificate) throws {
|
||||||
|
let updateQuery = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrAccount: certificate.id,
|
||||||
|
])
|
||||||
|
|
||||||
|
let cert = try JSONEncoder().encode(certificate.openSSHCertificate)
|
||||||
|
let updatedAttributes = KeychainDictionary([
|
||||||
|
kSecAttrGeneric: cert,
|
||||||
|
])
|
||||||
|
|
||||||
|
let status = SecItemUpdate(updateQuery, updatedAttributes)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
reloadCertificates()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func certificates(for secret: any Secret) -> [Certificate] {
|
||||||
|
certificates.filter { $0.openSSHCertificate.publicKey.data == secret.publicKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CertificateStore {
|
||||||
|
|
||||||
|
/// Loads all certificates from the store.
|
||||||
|
private func loadCertificates() {
|
||||||
|
let queryAttributes = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecUseDataProtectionKeychain: true,
|
||||||
|
kSecReturnData: true,
|
||||||
|
kSecMatchLimit: kSecMatchLimitAll,
|
||||||
|
kSecReturnAttributes: true
|
||||||
|
])
|
||||||
|
var untyped: CFTypeRef?
|
||||||
|
unsafe SecItemCopyMatching(queryAttributes, &untyped)
|
||||||
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let wrapped: [Certificate] = typed.compactMap {
|
||||||
|
do {
|
||||||
|
guard let data = $0[kSecValueData] as? Data,
|
||||||
|
let attributesData = $0[kSecAttrGeneric] as? Data else {
|
||||||
|
throw MissingAttributesError()
|
||||||
|
}
|
||||||
|
return Certificate(openSSHCertificate: try decoder.decode(OpenSSHCertificate.self, from: attributesData), rawData: data)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
if let validityRange = $0.validityRange {
|
||||||
|
validityRange.contains(Date())
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
certificates.append(contentsOf: wrapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CertificateStore {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let keyClass = kSecClassGenericPassword as String
|
||||||
|
static let keyTag = Data("com.maxgoedjen.certificatestore.opensshcertificate".utf8)
|
||||||
|
static let notificationToken = UUID().uuidString
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UnsupportedAlgorithmError: Error {}
|
||||||
|
struct MissingAttributesError: Error {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NSNotification.Name {
|
||||||
|
|
||||||
|
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed certificates)
|
||||||
|
public static let certificateStoreUpdated = NSNotification.Name("com.maxgoedjen.Secretive.certificateStore.updated")
|
||||||
|
// Internal notification that certificates were reloaded from the backing store.
|
||||||
|
public static let certificateStoreReloaded = NSNotification.Name("com.maxgoedjen.Secretive.certificateStore.reloaded")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import Foundation
|
||||||
|
import Formatters
|
||||||
|
|
||||||
|
public struct OpenSSHCertificate: Sendable, Codable, Equatable, Hashable, CustomDebugStringConvertible {
|
||||||
|
|
||||||
|
public var type: CertificateType
|
||||||
|
public var name: String
|
||||||
|
public var data: Data
|
||||||
|
|
||||||
|
public var publicKey: PublicKey
|
||||||
|
public var principals: [String]
|
||||||
|
public var keyID: String
|
||||||
|
public var serial: UInt64
|
||||||
|
public var validityRange: Range<Date>?
|
||||||
|
public var criticalOptions: [String]
|
||||||
|
public var extensions: [String]
|
||||||
|
public var signingKey: PublicKey
|
||||||
|
|
||||||
|
public init(
|
||||||
|
type: OpenSSHCertificate.CertificateType,
|
||||||
|
name: String,
|
||||||
|
data: Data,
|
||||||
|
publicKey: PublicKey,
|
||||||
|
principals: [String],
|
||||||
|
keyID: String,
|
||||||
|
serial: UInt64,
|
||||||
|
validityRange: Range<Date>? = nil,
|
||||||
|
criticalOptions: [String],
|
||||||
|
extensions: [String],
|
||||||
|
signingKey: PublicKey,
|
||||||
|
) {
|
||||||
|
self.type = type
|
||||||
|
self.name = name
|
||||||
|
self.data = data
|
||||||
|
self.publicKey = publicKey
|
||||||
|
self.principals = principals
|
||||||
|
self.keyID = keyID
|
||||||
|
self.serial = serial
|
||||||
|
self.validityRange = validityRange
|
||||||
|
self.criticalOptions = criticalOptions
|
||||||
|
self.extensions = extensions
|
||||||
|
self.signingKey = signingKey
|
||||||
|
}
|
||||||
|
|
||||||
|
public var debugDescription: String {
|
||||||
|
"OpenSSH Certificate \(name, default: "Unnamed"): \(data.formatted(.hex()))"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OpenSSHCertificate {
|
||||||
|
|
||||||
|
public enum CertificateType: String, Sendable, Codable {
|
||||||
|
case ecdsa256 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
|
||||||
|
case ecdsa384 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
|
||||||
|
case nistp521 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
|
||||||
|
|
||||||
|
public var keyIdentifier: String {
|
||||||
|
rawValue.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OpenSSHCertificate {
|
||||||
|
|
||||||
|
public struct PublicKey: Hashable, Sendable, Codable {
|
||||||
|
|
||||||
|
public let keyType: String
|
||||||
|
public let curveName: String
|
||||||
|
public let data: Data
|
||||||
|
|
||||||
|
public init(keyType: String, curveName: String, data: Data) {
|
||||||
|
self.keyType = keyType
|
||||||
|
self.curveName = curveName
|
||||||
|
self.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SSHProtocolKit
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
extension URL {
|
extension URL {
|
||||||
@@ -20,6 +21,10 @@ extension URL {
|
|||||||
agentHomeURL.appending(component: "PublicKeys")
|
agentHomeURL.appending(component: "PublicKeys")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var certificatesDirectory: URL {
|
||||||
|
agentHomeURL.appending(component: "Certificates")
|
||||||
|
}
|
||||||
|
|
||||||
/// The path for a Secret's public key.
|
/// The path for a Secret's public key.
|
||||||
/// - Parameter secret: The Secret to return the path for.
|
/// - Parameter secret: The Secret to return the path for.
|
||||||
/// - Returns: The path to the Secret's public key.
|
/// - Returns: The path to the Secret's public key.
|
||||||
@@ -30,6 +35,14 @@ extension URL {
|
|||||||
return directory.appending(component: "\(minimalHex).pub").path()
|
return directory.appending(component: "\(minimalHex).pub").path()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The path for a certificate.
|
||||||
|
/// - Parameter certificate: The Certificate to return the path for.
|
||||||
|
/// - Returns: The path to the Certificate.
|
||||||
|
/// - Warning: This method returning a path does not imply that a certificate has been written to disk already. This method only describes where it will be written to.
|
||||||
|
public static func certificatePath(for certificateID: String, in directory: URL) -> String {
|
||||||
|
return directory.appending(component: "\(certificateID)-cert.pub").path()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|||||||
74
Sources/Packages/Sources/Formatters/Data+Hex.swift
Normal file
74
Sources/Packages/Sources/Formatters/Data+Hex.swift
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
public struct HexDataStyle<SequenceType: Sequence>: Hashable, Codable {
|
||||||
|
|
||||||
|
let separator: String
|
||||||
|
|
||||||
|
public init(separator: String) {
|
||||||
|
self.separator = separator
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HexDataStyle: FormatStyle where SequenceType.Element == UInt8 {
|
||||||
|
|
||||||
|
public func format(_ value: SequenceType) -> String {
|
||||||
|
value
|
||||||
|
.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }
|
||||||
|
.joined(separator: separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FormatStyle where Self == HexDataStyle<Data> {
|
||||||
|
|
||||||
|
public static func hex(separator: String = "") -> HexDataStyle<Data> {
|
||||||
|
HexDataStyle(separator: separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FormatStyle where Self == HexDataStyle<Insecure.MD5Digest> {
|
||||||
|
|
||||||
|
public static func hex(separator: String = ":") -> HexDataStyle<Insecure.MD5Digest> {
|
||||||
|
HexDataStyle(separator: separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Base64DataStyle<SequenceType: Sequence>: Hashable, Codable {
|
||||||
|
|
||||||
|
private let stripPadding: Bool
|
||||||
|
|
||||||
|
public init(stripPadding: Bool) {
|
||||||
|
self.stripPadding = stripPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Base64DataStyle: FormatStyle where SequenceType.Element == UInt8 {
|
||||||
|
|
||||||
|
public func format(_ value: SequenceType) -> String {
|
||||||
|
let base64 = Data(value).base64EncodedString()
|
||||||
|
let paddingRange = base64.index(base64.endIndex, offsetBy: -2)..<base64.endIndex
|
||||||
|
return base64.replacingOccurrences(of: "=", with: "", range: paddingRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FormatStyle where Self == Base64DataStyle<Data> {
|
||||||
|
|
||||||
|
public static func base64(stripPadding: Bool) -> Base64DataStyle<Data> {
|
||||||
|
Base64DataStyle(stripPadding: stripPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FormatStyle where Self == Base64DataStyle<SHA256.Digest> {
|
||||||
|
|
||||||
|
public static func base64(stripPadding: Bool) -> Base64DataStyle<SHA256.Digest> {
|
||||||
|
Base64DataStyle(stripPadding: stripPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
public struct HexDataStyle<SequenceType: Sequence>: Hashable, Codable {
|
|
||||||
|
|
||||||
let separator: String
|
|
||||||
|
|
||||||
public init(separator: String) {
|
|
||||||
self.separator = separator
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HexDataStyle: FormatStyle where SequenceType.Element == UInt8 {
|
|
||||||
|
|
||||||
public func format(_ value: SequenceType) -> String {
|
|
||||||
value
|
|
||||||
.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }
|
|
||||||
.joined(separator: separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension FormatStyle where Self == HexDataStyle<Data> {
|
|
||||||
|
|
||||||
public static func hex(separator: String = "") -> HexDataStyle<Data> {
|
|
||||||
HexDataStyle(separator: separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
extension FormatStyle where Self == HexDataStyle<Insecure.MD5Digest> {
|
|
||||||
|
|
||||||
public static func hex(separator: String = ":") -> HexDataStyle<Insecure.MD5Digest> {
|
|
||||||
HexDataStyle(separator: separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
import CertificateKit
|
||||||
|
import Formatters
|
||||||
|
|
||||||
|
/// Generates OpenSSH representations of Certificates.
|
||||||
|
public struct OpenSSHCertificateWriter: Sendable {
|
||||||
|
|
||||||
|
/// Initializes the writer.
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates an OpenSSH data payload identifying the certificate.
|
||||||
|
/// - Returns: OpenSSH data payload identifying the certificate.
|
||||||
|
public func data(publicKey: OpenSSHCertificate.PublicKey) -> Data {
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
|
||||||
|
publicKey.keyType.lengthAndData +
|
||||||
|
publicKey.curveName.lengthAndData +
|
||||||
|
publicKey.data.lengthAndData
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates an OpenSSH SHA256 fingerprint string.
|
||||||
|
/// - Returns: OpenSSH SHA256 fingerprint string.
|
||||||
|
public func openSSHSHA256KeyFingerprint(publicKey: OpenSSHCertificate.PublicKey) -> String {
|
||||||
|
// OpenSSL format seems to strip the padding at the end.
|
||||||
|
let cleaned = SHA256.hash(data: data(publicKey: publicKey)).formatted(.base64(stripPadding: true))
|
||||||
|
return "SHA256:\(cleaned)"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import Foundation
|
||||||
|
import CertificateKit
|
||||||
|
|
||||||
|
public protocol OpenSSHCertificateParserProtocol {
|
||||||
|
func parse(data: Data) async throws -> OpenSSHCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct OpenSSHCertificateParser: OpenSSHCertificateParserProtocol, Sendable {
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
assert(Bundle.main.bundleURL.pathExtension == "xpc" || ProcessInfo.processInfo.processName == "xctest", "Potentially unsafe parsing code should run in an XPC service")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func parse(data: Data) throws(OpenSSHCertificateError) -> OpenSSHCertificate {
|
||||||
|
let string = String(decoding: data, as: UTF8.self)
|
||||||
|
var elements = string
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.components(separatedBy: " ")
|
||||||
|
guard elements.count >= 2 else {
|
||||||
|
throw OpenSSHCertificateError.parsingFailed
|
||||||
|
}
|
||||||
|
let typeString = elements.removeFirst()
|
||||||
|
guard let type = OpenSSHCertificate.CertificateType(rawValue: typeString) else { throw .unsupportedType }
|
||||||
|
let encodedKey = elements.removeFirst()
|
||||||
|
guard let decoded = Data(base64Encoded: encodedKey) else {
|
||||||
|
throw OpenSSHCertificateError.parsingFailed
|
||||||
|
}
|
||||||
|
let comment = elements.first
|
||||||
|
do {
|
||||||
|
let dataParser = OpenSSHReader(data: decoded)
|
||||||
|
let publicKeyType = try dataParser.readNextChunkAsString() // Theoretically the same as typeString, but
|
||||||
|
.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||||
|
_ = try dataParser.readNextChunk() // Nonce
|
||||||
|
let publicKeyCurveName = try dataParser.readNextChunkAsString()
|
||||||
|
let publicKeyData = try dataParser.readNextChunk()
|
||||||
|
let publicKey = OpenSSHCertificate.PublicKey(keyType: publicKeyType, curveName: publicKeyCurveName, data: publicKeyData)
|
||||||
|
let serialNumber = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true)
|
||||||
|
let role = try dataParser.readNextBytes(as: UInt32.self, convertEndianness: true)
|
||||||
|
_ = role
|
||||||
|
let keyIdentifier = try dataParser.readNextChunkAsString()
|
||||||
|
let principalsReader = try dataParser.readNextChunkAsSubReader()
|
||||||
|
var principals: [String] = []
|
||||||
|
while !principalsReader.done {
|
||||||
|
try principals.append(principalsReader.readNextChunkAsString())
|
||||||
|
}
|
||||||
|
let validAfter = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true)
|
||||||
|
let validBefore = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true)
|
||||||
|
let validityRange = Date(timeIntervalSince1970: TimeInterval(validAfter))..<Date(timeIntervalSince1970: TimeInterval(validBefore))
|
||||||
|
let criticalOptionsReader = try dataParser.readNextChunkAsSubReader()
|
||||||
|
var criticalOptions: [String] = []
|
||||||
|
while !criticalOptionsReader.done {
|
||||||
|
let next = try criticalOptionsReader.readNextChunkAsString()
|
||||||
|
if !next.isEmpty {
|
||||||
|
criticalOptions.append(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let extensionsReader = try dataParser.readNextChunkAsSubReader()
|
||||||
|
var extensions: [String] = []
|
||||||
|
while !extensionsReader.done {
|
||||||
|
let next = try extensionsReader.readNextChunkAsString()
|
||||||
|
if !next.isEmpty {
|
||||||
|
extensions.append(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = try dataParser.readNextChunk() // reserved
|
||||||
|
let signingKeyReader = try dataParser.readNextChunkAsSubReader()
|
||||||
|
let signingKeyType = try signingKeyReader.readNextChunkAsString()
|
||||||
|
let signingKeyCurveName = try signingKeyReader.readNextChunkAsString()
|
||||||
|
let signingKeyData = try signingKeyReader.readNextChunk()
|
||||||
|
let signingKey = OpenSSHCertificate.PublicKey(keyType: signingKeyType, curveName: signingKeyCurveName, data: signingKeyData)
|
||||||
|
|
||||||
|
return OpenSSHCertificate(
|
||||||
|
type: type,
|
||||||
|
name: comment ?? keyIdentifier,
|
||||||
|
data: decoded,
|
||||||
|
publicKey: publicKey,
|
||||||
|
principals: principals,
|
||||||
|
keyID: keyIdentifier,
|
||||||
|
serial: serialNumber,
|
||||||
|
validityRange: validityRange,
|
||||||
|
criticalOptions: criticalOptions,
|
||||||
|
extensions: extensions,
|
||||||
|
signingKey: signingKey,
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
throw .parsingFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OpenSSHCertificateError: Error, Codable {
|
||||||
|
case unsupportedType
|
||||||
|
case parsingFailed
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ public struct OpenSSHPublicKeyWriter: Sendable {
|
|||||||
("nistp" + String(describing: secret.keyType.size)).lengthAndData +
|
("nistp" + String(describing: secret.keyType.size)).lengthAndData +
|
||||||
secret.publicKey.lengthAndData
|
secret.publicKey.lengthAndData
|
||||||
case .mldsa:
|
case .mldsa:
|
||||||
// https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05
|
// https://www.ietf.org/archive/id/draft-sfluhrer-ssh-mldsa-04.txt
|
||||||
openSSHIdentifier(for: secret.keyType).lengthAndData +
|
openSSHIdentifier(for: secret.keyType).lengthAndData +
|
||||||
secret.publicKey.lengthAndData
|
secret.publicKey.lengthAndData
|
||||||
case .rsa:
|
case .rsa:
|
||||||
@@ -41,9 +41,7 @@ public struct OpenSSHPublicKeyWriter: Sendable {
|
|||||||
/// - Returns: OpenSSH SHA256 fingerprint string.
|
/// - Returns: OpenSSH SHA256 fingerprint string.
|
||||||
public func openSSHSHA256Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
|
public func openSSHSHA256Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
|
||||||
// OpenSSL format seems to strip the padding at the end.
|
// OpenSSL format seems to strip the padding at the end.
|
||||||
let base64 = Data(SHA256.hash(data: data(secret: secret))).base64EncodedString()
|
let cleaned = SHA256.hash(data: data(secret: secret)).formatted(.base64(stripPadding: true))
|
||||||
let paddingRange = base64.index(base64.endIndex, offsetBy: -2)..<base64.endIndex
|
|
||||||
let cleaned = base64.replacingOccurrences(of: "=", with: "", range: paddingRange)
|
|
||||||
return "SHA256:\(cleaned)"
|
return "SHA256:\(cleaned)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ public final class OpenSSHReader {
|
|||||||
/// - Parameter data: The data to read.
|
/// - Parameter data: The data to read.
|
||||||
public init(data: Data) {
|
public init(data: Data) {
|
||||||
remaining = Data(data)
|
remaining = Data(data)
|
||||||
|
if remaining.count == 0 {
|
||||||
|
done = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the next chunk of data from the playload.
|
/// Reads the next chunk of data from the playload.
|
||||||
@@ -39,18 +42,6 @@ public final class OpenSSHReader {
|
|||||||
return convertEndianness ? T(value.bigEndian) : T(value)
|
return convertEndianness ? T(value.bigEndian) : T(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func readNextByteAsBool() throws(OpenSSHReaderError) -> Bool {
|
|
||||||
let size = MemoryLayout<Bool>.size
|
|
||||||
guard remaining.count >= size else { throw .beyondBounds }
|
|
||||||
let lengthRange = 0..<size
|
|
||||||
let lengthChunk = remaining[lengthRange]
|
|
||||||
remaining.removeSubrange(lengthRange)
|
|
||||||
if remaining.isEmpty {
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
return unsafe lengthChunk.bytes.unsafeLoad(as: Bool.self)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
|
public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
|
||||||
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
|
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
|
||||||
}
|
}
|
||||||
@@ -62,6 +53,5 @@ public final class OpenSSHReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum OpenSSHReaderError: Error, Codable {
|
public enum OpenSSHReaderError: Error, Codable {
|
||||||
case incorrectFormat
|
|
||||||
case beyondBounds
|
case beyondBounds
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public struct OpenSSHSignatureWriter: Sendable {
|
|||||||
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
|
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
|
||||||
ecdsaSignature(signature, keyType: secret.keyType)
|
ecdsaSignature(signature, keyType: secret.keyType)
|
||||||
case .mldsa:
|
case .mldsa:
|
||||||
// https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05
|
// https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-00#name-public-key-algorithms
|
||||||
mldsaSignature(signature, keyType: secret.keyType)
|
mldsaSignature(signature, keyType: secret.keyType)
|
||||||
case .rsa:
|
case .rsa:
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import Foundation
|
||||||
|
import OSLog
|
||||||
|
import SecretKit
|
||||||
|
import CertificateKit
|
||||||
|
|
||||||
|
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() {
|
||||||
|
assert(Bundle.main.bundleURL.pathExtension == "xpc" || ProcessInfo.processInfo.processName == "xctest", "Potentially unsafe parsing code should run in an XPC service")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = try reader.readNextChunkAsString()
|
||||||
|
guard let certType = OpenSSHCertificate.CertificateType(rawValue: certType) else { return nil }
|
||||||
|
_ = try reader.readNextChunk() // nonce
|
||||||
|
let curveIdentifier = try reader.readNextChunk()
|
||||||
|
let publicKey = try reader.readNextChunk()
|
||||||
|
let openSSHIdentifier = certType.keyIdentifier
|
||||||
|
return openSSHIdentifier.lengthAndData +
|
||||||
|
curveIdentifier.lengthAndData +
|
||||||
|
publicKey.lengthAndData
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension SSHAgentInputParser {
|
||||||
|
|
||||||
|
public enum AgentParsingError: Error, Codable {
|
||||||
|
case unknownRequest
|
||||||
|
case unhandledRequest
|
||||||
|
case invalidData
|
||||||
|
case openSSHReader(OpenSSHReaderError)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ extension SSHAgent {
|
|||||||
case lock
|
case lock
|
||||||
case unlock
|
case unlock
|
||||||
case addSmartcardKeyConstrained
|
case addSmartcardKeyConstrained
|
||||||
case protocolExtension(ProtocolExtension)
|
case protocolExtension
|
||||||
case unknown(UInt8)
|
case unknown(UInt8)
|
||||||
|
|
||||||
public var protocolID: UInt8 {
|
public var protocolID: UInt8 {
|
||||||
@@ -60,82 +60,18 @@ extension SSHAgent {
|
|||||||
|
|
||||||
public struct SignatureRequestContext: Sendable, Codable {
|
public struct SignatureRequestContext: Sendable, Codable {
|
||||||
public let keyBlob: Data
|
public let keyBlob: Data
|
||||||
public let dataToSign: SignaturePayload
|
public let dataToSign: Data
|
||||||
|
|
||||||
public init(keyBlob: Data, dataToSign: SignaturePayload) {
|
public init(keyBlob: Data, dataToSign: Data) {
|
||||||
self.keyBlob = keyBlob
|
self.keyBlob = keyBlob
|
||||||
self.dataToSign = dataToSign
|
self.dataToSign = dataToSign
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var empty: SignatureRequestContext {
|
public static var empty: SignatureRequestContext {
|
||||||
SignatureRequestContext(keyBlob: Data(), dataToSign: SignaturePayload(raw: Data(), decoded: nil))
|
SignatureRequestContext(keyBlob: Data(), dataToSign: Data())
|
||||||
}
|
|
||||||
|
|
||||||
public struct SignaturePayload: Sendable, Codable {
|
|
||||||
|
|
||||||
public let raw: Data
|
|
||||||
public let decoded: DecodedPayload?
|
|
||||||
|
|
||||||
public init(
|
|
||||||
raw: Data,
|
|
||||||
decoded: DecodedPayload?
|
|
||||||
) {
|
|
||||||
self.raw = raw
|
|
||||||
self.decoded = decoded
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum DecodedPayload: Sendable, Codable {
|
|
||||||
case sshConnection(SSHConnectionPayload)
|
|
||||||
case sshSig(SSHSigPayload)
|
|
||||||
|
|
||||||
public struct SSHConnectionPayload: Sendable, Codable {
|
|
||||||
|
|
||||||
public let username: String
|
|
||||||
public let hasSignature: Bool
|
|
||||||
public let publicKeyAlgorithm: String
|
|
||||||
public let publicKey: Data
|
|
||||||
public let hostKey: Data
|
|
||||||
|
|
||||||
public init(
|
|
||||||
username: String,
|
|
||||||
hasSignature: Bool,
|
|
||||||
publicKeyAlgorithm: String,
|
|
||||||
publicKey: Data,
|
|
||||||
hostKey: Data
|
|
||||||
) {
|
|
||||||
self.username = username
|
|
||||||
self.hasSignature = hasSignature
|
|
||||||
self.publicKeyAlgorithm = publicKeyAlgorithm
|
|
||||||
self.publicKey = publicKey
|
|
||||||
self.hostKey = hostKey
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct SSHSigPayload: Sendable, Codable {
|
|
||||||
|
|
||||||
public let namespace: String
|
|
||||||
public let hashAlgorithm: String
|
|
||||||
public let hash: Data
|
|
||||||
|
|
||||||
public init(
|
|
||||||
namespace: String,
|
|
||||||
hashAlgorithm: String,
|
|
||||||
hash: Data,
|
|
||||||
) {
|
|
||||||
self.namespace = namespace
|
|
||||||
self.hashAlgorithm = hashAlgorithm
|
|
||||||
self.hash = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
|
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
|
||||||
@@ -152,8 +88,8 @@ extension SSHAgent {
|
|||||||
switch self {
|
switch self {
|
||||||
case .agentFailure: "SSH_AGENT_FAILURE"
|
case .agentFailure: "SSH_AGENT_FAILURE"
|
||||||
case .agentSuccess: "SSH_AGENT_SUCCESS"
|
case .agentSuccess: "SSH_AGENT_SUCCESS"
|
||||||
case .agentIdentitiesAnswer: "SSH2_AGENT_IDENTITIES_ANSWER"
|
case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER"
|
||||||
case .agentSignResponse: "SSH2_AGENT_SIGN_RESPONSE"
|
case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE"
|
||||||
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
|
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
|
||||||
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
|
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
// Extensions, as defined in https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.agent
|
|
||||||
|
|
||||||
extension SSHAgent {
|
|
||||||
|
|
||||||
public enum ProtocolExtension: CustomDebugStringConvertible, Codable, Sendable {
|
|
||||||
case openSSH(OpenSSHExtension)
|
|
||||||
case unknown(String)
|
|
||||||
|
|
||||||
public var debugDescription: String {
|
|
||||||
switch self {
|
|
||||||
case let .openSSH(protocolExtension):
|
|
||||||
protocolExtension.debugDescription
|
|
||||||
case .unknown(let string):
|
|
||||||
"Unknown (\(string))"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var empty: ProtocolExtension {
|
|
||||||
.unknown("empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ProtocolExtensionParsingError: Error {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SSHAgent.ProtocolExtension {
|
|
||||||
|
|
||||||
public enum OpenSSHExtension: CustomDebugStringConvertible, Codable, Sendable {
|
|
||||||
case sessionBind(SessionBindContext)
|
|
||||||
case unknown(String)
|
|
||||||
|
|
||||||
public static var domain: String {
|
|
||||||
"openssh.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
public var name: String {
|
|
||||||
switch self {
|
|
||||||
case .sessionBind:
|
|
||||||
"session-bind"
|
|
||||||
case .unknown(let name):
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var debugDescription: String {
|
|
||||||
"\(name)@\(OpenSSHExtension.domain)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SSHAgent.ProtocolExtension.OpenSSHExtension {
|
|
||||||
|
|
||||||
public struct SessionBindContext: Codable, Sendable {
|
|
||||||
|
|
||||||
public let hostKey: Data
|
|
||||||
public let sessionID: Data
|
|
||||||
public let signature: Data
|
|
||||||
public let forwarding: Bool
|
|
||||||
|
|
||||||
public init(hostKey: Data, sessionID: Data, signature: Data, forwarding: Bool) {
|
|
||||||
self.hostKey = hostKey
|
|
||||||
self.sessionID = sessionID
|
|
||||||
self.signature = signature
|
|
||||||
self.forwarding = forwarding
|
|
||||||
}
|
|
||||||
|
|
||||||
public static let empty = SessionBindContext(hostKey: Data(), sessionID: Data(), signature: Data(), forwarding: false)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ import Foundation
|
|||||||
import CryptoKit
|
import CryptoKit
|
||||||
import OSLog
|
import OSLog
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
import CertificateKit
|
||||||
import AppKit
|
import AppKit
|
||||||
import SSHProtocolKit
|
import SSHProtocolKit
|
||||||
|
|
||||||
@@ -9,25 +10,21 @@ import SSHProtocolKit
|
|||||||
public final class Agent: Sendable {
|
public final class Agent: Sendable {
|
||||||
|
|
||||||
private let storeList: SecretStoreList
|
private let storeList: SecretStoreList
|
||||||
|
private let certificateStore: CertificateStore
|
||||||
private let witness: SigningWitness?
|
private let witness: SigningWitness?
|
||||||
private let publicKeyWriter = OpenSSHPublicKeyWriter()
|
private let publicKeyWriter = OpenSSHPublicKeyWriter()
|
||||||
private let signatureWriter = OpenSSHSignatureWriter()
|
private let signatureWriter = OpenSSHSignatureWriter()
|
||||||
private let certificateHandler = OpenSSHCertificateHandler()
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
||||||
|
|
||||||
@MainActor private var sessionID: SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext?
|
|
||||||
|
|
||||||
/// Initializes an agent with a store list and a witness.
|
/// Initializes an agent with a store list and a witness.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - storeList: The `SecretStoreList` to make available.
|
/// - storeList: The `SecretStoreList` to make available.
|
||||||
/// - witness: A witness to notify of requests.
|
/// - witness: A witness to notify of requests.
|
||||||
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
public init(storeList: SecretStoreList, certificateStore: CertificateStore, witness: SigningWitness? = nil) {
|
||||||
logger.debug("Agent is running")
|
logger.debug("Agent is running")
|
||||||
self.storeList = storeList
|
self.storeList = storeList
|
||||||
|
self.certificateStore = certificateStore
|
||||||
self.witness = witness
|
self.witness = witness
|
||||||
Task { @MainActor in
|
|
||||||
await certificateHandler.reloadCertificates(for: storeList.allSecrets)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -35,7 +32,6 @@ public final class Agent: Sendable {
|
|||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data {
|
public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data {
|
||||||
logger.debug("Agent received request of type \(request.debugDescription)")
|
|
||||||
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
|
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
|
||||||
await reloadSecretsIfNeccessary()
|
await reloadSecretsIfNeccessary()
|
||||||
var response = Data()
|
var response = Data()
|
||||||
@@ -46,34 +42,9 @@ extension Agent {
|
|||||||
response.append(await identities())
|
response.append(await identities())
|
||||||
logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)")
|
||||||
case .signRequest(let context):
|
case .signRequest(let context):
|
||||||
if let boundSession = await sessionID {
|
|
||||||
switch context.dataToSign.decoded {
|
|
||||||
case .sshConnection(let payload):
|
|
||||||
guard payload.hostKey == boundSession.hostKey else {
|
|
||||||
logger.error("Agent received bind request, but host key does not match signature reqeust host key.")
|
|
||||||
throw BindingFailure()
|
|
||||||
}
|
|
||||||
case .sshSig:
|
|
||||||
// SSHSIG does not have a host binding payload.
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.append(SSHAgent.Response.agentSignResponse.data)
|
response.append(SSHAgent.Response.agentSignResponse.data)
|
||||||
response.append(try await sign(data: context.dataToSign.raw, keyBlob: context.keyBlob, provenance: provenance))
|
response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance))
|
||||||
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
|
||||||
case .protocolExtension(.openSSH(.sessionBind(let bind))):
|
|
||||||
response = try await MainActor.run {
|
|
||||||
guard sessionID == nil else {
|
|
||||||
logger.error("Agent received bind request, but already bound.")
|
|
||||||
throw BindingFailure()
|
|
||||||
}
|
|
||||||
logger.debug("Agent bound")
|
|
||||||
sessionID = bind
|
|
||||||
return SSHAgent.Response.agentSuccess.data
|
|
||||||
}
|
|
||||||
logger.debug("Agent returned \(SSHAgent.Response.agentSuccess.debugDescription)")
|
|
||||||
case .unknown(let value):
|
case .unknown(let value):
|
||||||
logger.error("Agent received unknown request of type \(value).")
|
logger.error("Agent received unknown request of type \(value).")
|
||||||
throw UnhandledRequestError()
|
throw UnhandledRequestError()
|
||||||
@@ -96,7 +67,6 @@ extension Agent {
|
|||||||
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
|
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
|
||||||
func identities() async -> Data {
|
func identities() async -> Data {
|
||||||
let secrets = await storeList.allSecrets
|
let secrets = await storeList.allSecrets
|
||||||
await certificateHandler.reloadCertificates(for: secrets)
|
|
||||||
var count = 0
|
var count = 0
|
||||||
var keyData = Data()
|
var keyData = Data()
|
||||||
|
|
||||||
@@ -105,10 +75,9 @@ extension Agent {
|
|||||||
keyData.append(keyBlob.lengthAndData)
|
keyData.append(keyBlob.lengthAndData)
|
||||||
keyData.append(publicKeyWriter.comment(secret: secret).lengthAndData)
|
keyData.append(publicKeyWriter.comment(secret: secret).lengthAndData)
|
||||||
count += 1
|
count += 1
|
||||||
|
for certificate in await certificateStore.certificates(for: secret) {
|
||||||
if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) {
|
keyData.append(certificate.data.lengthAndData)
|
||||||
keyData.append(certificateData.lengthAndData)
|
keyData.append(certificate.name.lengthAndData)
|
||||||
keyData.append(name.lengthAndData)
|
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +94,7 @@ extension Agent {
|
|||||||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||||
func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||||
guard let (secret, store) = await secret(matching: keyBlob) else {
|
guard let (secret, store) = await secret(matching: keyBlob) else {
|
||||||
let keyBlobHex = keyBlob.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }.joined()
|
let keyBlobHex = keyBlob.formatted(.hex())
|
||||||
logger.debug("Agent did not have a key matching \(keyBlobHex)")
|
logger.debug("Agent did not have a key matching \(keyBlobHex)")
|
||||||
throw NoMatchingKeyError()
|
throw NoMatchingKeyError()
|
||||||
}
|
}
|
||||||
@@ -173,7 +142,6 @@ extension Agent {
|
|||||||
|
|
||||||
struct NoMatchingKeyError: Error {}
|
struct NoMatchingKeyError: Error {}
|
||||||
struct UnhandledRequestError: Error {}
|
struct UnhandledRequestError: Error {}
|
||||||
struct BindingFailure: Error {}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import OSLog
|
|
||||||
import SecretKit
|
|
||||||
import SSHProtocolKit
|
|
||||||
|
|
||||||
/// Manages storage and lookup for OpenSSH certificates.
|
|
||||||
public actor OpenSSHCertificateHandler: Sendable {
|
|
||||||
|
|
||||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory)
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
|
|
||||||
private let writer = OpenSSHPublicKeyWriter()
|
|
||||||
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
|
|
||||||
|
|
||||||
/// Initializes an OpenSSHCertificateHandler.
|
|
||||||
public init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reloads any certificates in the PublicKeys folder.
|
|
||||||
/// - Parameter secrets: the secrets to look up corresponding certificates for.
|
|
||||||
public func reloadCertificates(for secrets: [AnySecret]) {
|
|
||||||
guard publicKeyFileStoreController.hasAnyCertificates else {
|
|
||||||
logger.log("No certificates, short circuiting")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyBlobsAndNames = secrets.reduce(into: [:]) { partialResult, next in
|
|
||||||
partialResult[next] = try? loadKeyblobAndName(for: next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
|
||||||
keyBlobsAndNames[AnySecret(secret)]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
private func loadKeyblobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
|
||||||
let certificatePath = publicKeyFileStoreController.sshCertificatePath(for: secret)
|
|
||||||
guard FileManager.default.fileExists(atPath: certificatePath) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Found certificate for \(secret.name)")
|
|
||||||
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
|
|
||||||
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
|
|
||||||
|
|
||||||
guard certElements.count >= 2 else {
|
|
||||||
logger.warning("Certificate found for \(secret.name) but failed to load")
|
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
|
||||||
}
|
|
||||||
guard let certDecoded = Data(base64Encoded: certElements[1] as String) else {
|
|
||||||
logger.warning("Certificate found for \(secret.name) but failed to decode base64 key")
|
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
if certElements.count >= 3 {
|
|
||||||
let certName = Data(certElements[2].utf8)
|
|
||||||
return (certDecoded, certName)
|
|
||||||
}
|
|
||||||
let certName = Data(secret.name.utf8)
|
|
||||||
logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
|
|
||||||
return (certDecoded, certName)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension OpenSSHCertificateHandler {
|
|
||||||
|
|
||||||
enum OpenSSHCertificateError: LocalizedError {
|
|
||||||
case unsupportedType
|
|
||||||
case parsingFailed
|
|
||||||
case doesNotExist
|
|
||||||
|
|
||||||
public var errorDescription: String? {
|
|
||||||
switch self {
|
|
||||||
case .unsupportedType:
|
|
||||||
return "The key type was unsupported"
|
|
||||||
case .parsingFailed:
|
|
||||||
return "Failed to properly parse the SSH certificate"
|
|
||||||
case .doesNotExist:
|
|
||||||
return "Certificate does not exist"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,21 @@ import Foundation
|
|||||||
import OSLog
|
import OSLog
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import SSHProtocolKit
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
import Common
|
import Common
|
||||||
|
|
||||||
/// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts.
|
/// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts.
|
||||||
public final class PublicKeyFileStoreController: Sendable {
|
public final class PublicKeyFileStoreController: Sendable {
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
|
||||||
private let directory: URL
|
private let publicKeysURL: URL
|
||||||
|
private let certificatesURL: URL
|
||||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||||
|
|
||||||
/// Initializes a PublicKeyFileStoreController.
|
/// Initializes a PublicKeyFileStoreController.
|
||||||
public init(directory: URL) {
|
public init(publicKeysURL: URL, certificatesURL: URL) {
|
||||||
self.directory = directory
|
self.publicKeysURL = publicKeysURL
|
||||||
|
self.certificatesURL = certificatesURL
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes out the keys specified to disk.
|
/// Writes out the keys specified to disk.
|
||||||
@@ -22,10 +25,10 @@ public final class PublicKeyFileStoreController: Sendable {
|
|||||||
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
|
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
|
||||||
logger.log("Writing public keys to disk")
|
logger.log("Writing public keys to disk")
|
||||||
if clear {
|
if clear {
|
||||||
let validPaths = Set(secrets.map { URL.publicKeyPath(for: $0, in: directory) })
|
let validPaths = Set(secrets.map { URL.publicKeyPath(for: $0, in: publicKeysURL) })
|
||||||
.union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
.union(Set(secrets.map { legacySSHCertificatePath(for: $0) }))
|
||||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
|
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: publicKeysURL.path())) ?? []
|
||||||
let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() }
|
let fullPathContents = contentsOfDirectory.map { publicKeysURL.appending(path: $0).path() }
|
||||||
|
|
||||||
let untracked = Set(fullPathContents)
|
let untracked = Set(fullPathContents)
|
||||||
.subtracting(validPaths)
|
.subtracting(validPaths)
|
||||||
@@ -34,35 +37,47 @@ public final class PublicKeyFileStoreController: Sendable {
|
|||||||
try? FileManager.default.removeItem(at: URL(string: path)!)
|
try? FileManager.default.removeItem(at: URL(string: path)!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
|
try? FileManager.default.createDirectory(at: publicKeysURL, withIntermediateDirectories: false, attributes: nil)
|
||||||
for secret in secrets {
|
for secret in secrets {
|
||||||
let path = URL.publicKeyPath(for: secret, in: directory)
|
let path = URL.publicKeyPath(for: secret, in: publicKeysURL)
|
||||||
let data = Data(keyWriter.openSSHString(secret: secret).utf8)
|
let data = Data(keyWriter.openSSHString(secret: secret).utf8)
|
||||||
FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
|
FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
|
||||||
}
|
}
|
||||||
logger.log("Finished writing public keys")
|
logger.log("Finished writing public keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes out the certificates specified to disk.
|
||||||
|
/// - Parameter certificates: The Secrets to generate keys for.
|
||||||
|
/// - Parameter clear: Whether or not any untracked files in the directory should be removed.
|
||||||
|
public func generateCertificates(for certificates: [Certificate], clear: Bool = false) throws {
|
||||||
|
logger.log("Writing certificates to disk")
|
||||||
|
if clear {
|
||||||
|
let validPaths = Set(certificates.map { URL.certificatePath(for: $0.id, in: certificatesURL) })
|
||||||
|
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: certificatesURL.path())) ?? []
|
||||||
|
let fullPathContents = contentsOfDirectory.map { certificatesURL.appending(path: $0).path() }
|
||||||
|
|
||||||
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
|
let untracked = Set(fullPathContents)
|
||||||
public var hasAnyCertificates: Bool {
|
.subtracting(validPaths)
|
||||||
do {
|
for path in untracked {
|
||||||
return try FileManager.default
|
// string instead of fileURLWithPath since we're already using fileURL format.
|
||||||
.contentsOfDirectory(atPath: directory.path())
|
try? FileManager.default.removeItem(at: URL(string: path)!)
|
||||||
.filter { $0.hasSuffix("-cert.pub") }
|
}
|
||||||
.isEmpty == false
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
try? FileManager.default.createDirectory(at: certificatesURL, withIntermediateDirectories: false, attributes: nil)
|
||||||
|
for certificate in certificates {
|
||||||
|
let path = URL.certificatePath(for: certificate.id, in: certificatesURL)
|
||||||
|
FileManager.default.createFile(atPath: path, contents: certificate.rawData, attributes: nil)
|
||||||
|
}
|
||||||
|
logger.log("Finished writing certificates")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The path for a Secret's SSH Certificate public key.
|
/// The path for a Secret's SSH Certificate public key.
|
||||||
/// - Parameter secret: The Secret to return the path for.
|
/// - Parameter secret: The Secret to return the path for.
|
||||||
/// - Returns: The path to the SSH Certificate public key.
|
/// - Returns: The path to the SSH Certificate public key.
|
||||||
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
|
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
|
||||||
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
private func legacySSHCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||||
return directory.appending(component: "\(minimalHex)-cert.pub").path()
|
return publicKeysURL.appending(component: "\(minimalHex).pub").path()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,253 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import OSLog
|
|
||||||
import SecretKit
|
|
||||||
import SSHProtocolKit
|
|
||||||
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
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(.empty).protocolID:
|
|
||||||
return .protocolExtension(try protocolExtension(from: body))
|
|
||||||
default:
|
|
||||||
return .unknown(rawRequestInt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SSHAgentInputParser {
|
|
||||||
|
|
||||||
private enum Constants {
|
|
||||||
static let userAuthMagic: UInt8 = 50 // SSH2_MSG_USERAUTH_REQUEST
|
|
||||||
static let sshSigMagic = Data("SSHSIG".utf8)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 rawPayload = try reader.readNextChunk()
|
|
||||||
let payload: SSHAgent.Request.SignatureRequestContext.SignaturePayload
|
|
||||||
do {
|
|
||||||
if rawPayload.count > 6 && rawPayload[0..<6] == Constants.sshSigMagic {
|
|
||||||
payload = .init(raw: rawPayload, decoded: .sshSig(try sshSigPayload(from: rawPayload[6...])))
|
|
||||||
} else {
|
|
||||||
payload = .init(raw: rawPayload, decoded: .sshConnection(try sshConnectionPayload(from: rawPayload)))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
payload = .init(raw: rawPayload, decoded: nil)
|
|
||||||
}
|
|
||||||
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sshSigPayload(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext.SignaturePayload.DecodedPayload.SSHSigPayload {
|
|
||||||
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L79
|
|
||||||
let payloadReader = OpenSSHReader(data: data)
|
|
||||||
let namespace = try payloadReader.readNextChunkAsString()
|
|
||||||
_ = try payloadReader.readNextChunk() // reserved
|
|
||||||
let hashAlgorithm = try payloadReader.readNextChunkAsString()
|
|
||||||
let hash = try payloadReader.readNextChunk()
|
|
||||||
return .init(
|
|
||||||
namespace: namespace,
|
|
||||||
hashAlgorithm: hashAlgorithm,
|
|
||||||
hash: hash
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sshConnectionPayload(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext.SignaturePayload.DecodedPayload.SSHConnectionPayload {
|
|
||||||
let payloadReader = OpenSSHReader(data: data)
|
|
||||||
_ = try payloadReader.readNextChunk()
|
|
||||||
let magic = try payloadReader.readNextBytes(as: UInt8.self, convertEndianness: false)
|
|
||||||
guard magic == Constants.userAuthMagic else { throw .incorrectFormat }
|
|
||||||
let username = try payloadReader.readNextChunkAsString()
|
|
||||||
_ = try payloadReader.readNextChunkAsString() // "ssh-connection"
|
|
||||||
_ = try payloadReader.readNextChunkAsString() // "publickey-hostbound-v00@openssh.com"
|
|
||||||
let hasSignature = try payloadReader.readNextByteAsBool()
|
|
||||||
let algorithm = try payloadReader.readNextChunkAsString()
|
|
||||||
let publicKeyReader = try payloadReader.readNextChunkAsSubReader()
|
|
||||||
_ = try publicKeyReader.readNextChunk()
|
|
||||||
_ = try publicKeyReader.readNextChunk()
|
|
||||||
let publicKey = try publicKeyReader.readNextChunk()
|
|
||||||
let hostKeyReader = try payloadReader.readNextChunkAsSubReader()
|
|
||||||
_ = try hostKeyReader.readNextChunk()
|
|
||||||
let hostKey = try hostKeyReader.readNextChunk()
|
|
||||||
return .init(
|
|
||||||
username: username,
|
|
||||||
hasSignature: hasSignature,
|
|
||||||
publicKeyAlgorithm: algorithm,
|
|
||||||
publicKey: publicKey,
|
|
||||||
hostKey: hostKey,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func protocolExtension(from data: Data) throws(AgentParsingError) -> SSHAgent.ProtocolExtension {
|
|
||||||
do {
|
|
||||||
let reader = OpenSSHReader(data: data)
|
|
||||||
let nameRaw = try reader.readNextChunkAsString()
|
|
||||||
let nameSplit = nameRaw.split(separator: "@")
|
|
||||||
guard nameSplit.count == 2 else {
|
|
||||||
throw AgentParsingError.invalidData
|
|
||||||
}
|
|
||||||
let (name, domain) = (nameSplit[0], nameSplit[1])
|
|
||||||
switch domain {
|
|
||||||
case SSHAgent.ProtocolExtension.OpenSSHExtension.domain:
|
|
||||||
switch name {
|
|
||||||
case SSHAgent.ProtocolExtension.OpenSSHExtension.sessionBind(.empty).name:
|
|
||||||
let hostkeyBlob = try reader.readNextChunkAsSubReader()
|
|
||||||
let hostKeyType = try hostkeyBlob.readNextChunkAsString()
|
|
||||||
let hostKeyData = try hostkeyBlob.readNextChunk()
|
|
||||||
let sessionID = try reader.readNextChunk()
|
|
||||||
let signatureBlob = try reader.readNextChunkAsSubReader()
|
|
||||||
_ = try signatureBlob.readNextChunk() // key type again
|
|
||||||
let signature = try signatureBlob.readNextChunk()
|
|
||||||
let forwarding = try reader.readNextByteAsBool()
|
|
||||||
switch hostKeyType {
|
|
||||||
// FIXME: FACTOR OUT?
|
|
||||||
case "ssh-ed25519":
|
|
||||||
let hostKey = try CryptoKit.Curve25519.Signing.PublicKey(rawRepresentation: hostKeyData)
|
|
||||||
guard hostKey.isValidSignature(signature, for: sessionID) else {
|
|
||||||
throw AgentParsingError.incorrectSignature
|
|
||||||
}
|
|
||||||
case "ecdsa-sha2-nistp256":
|
|
||||||
let hostKey = try CryptoKit.P256.Signing.PublicKey(rawRepresentation: hostKeyData)
|
|
||||||
guard hostKey.isValidSignature(try .init(rawRepresentation: signature), for: sessionID) else {
|
|
||||||
throw AgentParsingError.incorrectSignature
|
|
||||||
}
|
|
||||||
case "ecdsa-sha2-nistp384":
|
|
||||||
let hostKey = try CryptoKit.P384.Signing.PublicKey(rawRepresentation: hostKeyData)
|
|
||||||
guard hostKey.isValidSignature(try .init(rawRepresentation: signature), for: sessionID) else {
|
|
||||||
throw AgentParsingError.incorrectSignature
|
|
||||||
}
|
|
||||||
case "ssh-mldsa-65":
|
|
||||||
if #available(macOS 26.0, *) {
|
|
||||||
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
|
|
||||||
guard hostKey.isValidSignature(signature, for: sessionID) else {
|
|
||||||
throw AgentParsingError.incorrectSignature
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw AgentParsingError.unhandledRequest
|
|
||||||
}
|
|
||||||
case "ssh-mldsa-87":
|
|
||||||
if #available(macOS 26.0, *) {
|
|
||||||
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
|
|
||||||
guard hostKey.isValidSignature(signature, for: sessionID) else {
|
|
||||||
throw AgentParsingError.incorrectSignature
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw AgentParsingError.unhandledRequest
|
|
||||||
}
|
|
||||||
case "ssh-rsa":
|
|
||||||
// FIXME: HANDLE
|
|
||||||
throw AgentParsingError.unhandledRequest
|
|
||||||
default:
|
|
||||||
throw AgentParsingError.unhandledRequest
|
|
||||||
}
|
|
||||||
let context = SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext(
|
|
||||||
hostKey: hostKeyData,
|
|
||||||
sessionID: sessionID,
|
|
||||||
signature: signature,
|
|
||||||
forwarding: forwarding
|
|
||||||
)
|
|
||||||
return .openSSH(.sessionBind(context))
|
|
||||||
default:
|
|
||||||
return .openSSH(.unknown(String(name)))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return .unknown(nameRaw)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch let error as OpenSSHReaderError {
|
|
||||||
throw .openSSHReader(error)
|
|
||||||
} catch let error as AgentParsingError {
|
|
||||||
throw error
|
|
||||||
} catch {
|
|
||||||
throw .unknownRequest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 incorrectSignature
|
|
||||||
case openSSHReader(OpenSSHReaderError)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
import launch
|
||||||
|
|
||||||
/// A controller that manages socket configuration and request dispatching.
|
/// A controller that manages socket configuration and request dispatching.
|
||||||
public struct SocketController {
|
public struct SocketController {
|
||||||
@@ -12,7 +13,8 @@ public struct SocketController {
|
|||||||
private let sessionsContinuation: AsyncStream<Session>.Continuation
|
private let sessionsContinuation: AsyncStream<Session>.Continuation
|
||||||
|
|
||||||
/// The active SocketPort. Must be retained to be kept valid.
|
/// The active SocketPort. Must be retained to be kept valid.
|
||||||
private let port: SocketPort
|
/// Only applicable for legacy non-launchd sockets.
|
||||||
|
private let port: SocketPort?
|
||||||
|
|
||||||
/// The FileHandle for the main socket.
|
/// The FileHandle for the main socket.
|
||||||
private let fileHandle: FileHandle
|
private let fileHandle: FileHandle
|
||||||
@@ -23,19 +25,41 @@ public struct SocketController {
|
|||||||
/// Tracer which determines who originates a socket connection.
|
/// Tracer which determines who originates a socket connection.
|
||||||
private let requestTracer = SigningRequestTracer()
|
private let requestTracer = SigningRequestTracer()
|
||||||
|
|
||||||
/// Initializes a socket controller with a specified path.
|
public enum Socket {
|
||||||
/// - Parameter path: The path to use as a socket.
|
case launchd(String)
|
||||||
public init(path: String) {
|
case path(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(_ socket: Socket) {
|
||||||
(sessions, sessionsContinuation) = AsyncStream<Session>.makeStream()
|
(sessions, sessionsContinuation) = AsyncStream<Session>.makeStream()
|
||||||
logger.debug("Socket controller setting up at \(path)")
|
switch socket {
|
||||||
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
case .path(let path):
|
||||||
logger.debug("Socket controller removed existing socket")
|
logger.debug("Socket controller setting up at \(path)")
|
||||||
|
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
||||||
|
logger.debug("Socket controller removed existing socket")
|
||||||
|
}
|
||||||
|
let exists = FileManager.default.fileExists(atPath: path)
|
||||||
|
assert(!exists)
|
||||||
|
logger.debug("Socket controller path is clear")
|
||||||
|
let port = SocketPort(path: path)
|
||||||
|
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
||||||
|
self.port = port
|
||||||
|
logger.debug("Socket listening at \(path)")
|
||||||
|
case .launchd(let name):
|
||||||
|
logger.debug("Socket controller setting for launchd-controlled socket \(name)")
|
||||||
|
port = nil
|
||||||
|
var fileDescriptors: UnsafeMutablePointer<Int32>? = nil
|
||||||
|
var count = 0
|
||||||
|
let result = unsafe launch_activate_socket(name, &fileDescriptors, &count)
|
||||||
|
guard result == kOSReturnSuccess, let socket = unsafe fileDescriptors?.pointee else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
fileHandle = FileHandle(fileDescriptor: socket, closeOnDealloc: true)
|
||||||
}
|
}
|
||||||
let exists = FileManager.default.fileExists(atPath: path)
|
listen()
|
||||||
assert(!exists)
|
}
|
||||||
logger.debug("Socket controller path is clear")
|
|
||||||
port = SocketPort(path: path)
|
func listen() {
|
||||||
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
|
||||||
Task { @MainActor [fileHandle, sessionsContinuation, logger] in
|
Task { @MainActor [fileHandle, sessionsContinuation, logger] in
|
||||||
// Create the sequence before triggering the notification to
|
// Create the sequence before triggering the notification to
|
||||||
// ensure it will not be missed.
|
// ensure it will not be missed.
|
||||||
@@ -51,7 +75,7 @@ public struct SocketController {
|
|||||||
fileHandle.acceptConnectionInBackgroundAndNotify()
|
fileHandle.acceptConnectionInBackgroundAndNotify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug("Socket listening at \(path)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -94,7 +118,7 @@ extension SocketController {
|
|||||||
guard !data.isEmpty else {
|
guard !data.isEmpty else {
|
||||||
logger.debug("Socket controller received empty data, ending continuation.")
|
logger.debug("Socket controller received empty data, ending continuation.")
|
||||||
messagesContinuation.finish()
|
messagesContinuation.finish()
|
||||||
try fileHandle.close()
|
try? fileHandle.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
messagesContinuation.yield(data)
|
messagesContinuation.yield(data)
|
||||||
@@ -125,14 +149,7 @@ private extension SocketPort {
|
|||||||
|
|
||||||
convenience init(path: String) {
|
convenience init(path: String) {
|
||||||
var addr = sockaddr_un()
|
var addr = sockaddr_un()
|
||||||
|
let length = addr.setPath(path)
|
||||||
let length = unsafe withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
|
|
||||||
unsafe path.withCString { cstring in
|
|
||||||
let len = unsafe strlen(cstring)
|
|
||||||
unsafe strncpy(pointer, cstring, len)
|
|
||||||
return len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This doesn't seem to be _strictly_ neccessary with SocketPort.
|
// This doesn't seem to be _strictly_ neccessary with SocketPort.
|
||||||
// but just for good form.
|
// but just for good form.
|
||||||
addr.sun_family = sa_family_t(AF_UNIX)
|
addr.sun_family = sa_family_t(AF_UNIX)
|
||||||
@@ -144,3 +161,31 @@ private extension SocketPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension sockaddr_un {
|
||||||
|
|
||||||
|
mutating func setPath(_ path: String) -> Int {
|
||||||
|
#if compiler(<6.4)
|
||||||
|
unsafe withUnsafeMutablePointer(to: &self.sun_path.0) { pointer in
|
||||||
|
unsafe path.withCString { cstring in
|
||||||
|
let len = unsafe strlen(cstring)
|
||||||
|
unsafe strncpy(pointer, cstring, len)
|
||||||
|
return len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
withUnsafeMutablePointer(to: &self.sun_path.0) { pointer in
|
||||||
|
path.withCString { cstring in
|
||||||
|
let len = unsafe strlen(cstring)
|
||||||
|
unsafe strncpy(pointer, cstring, len)
|
||||||
|
return len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changes the header from `UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>?` -> `UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>!`
|
||||||
|
@_silgen_name("launch_activate_socket")
|
||||||
|
func launch_activate_socket(_ name: UnsafePointer<CChar>, _ fds: UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>!, _ cnt: UnsafeMutablePointer<Int>!) -> Int32
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
import CryptoTokenKit
|
||||||
|
import CryptoKit
|
||||||
|
import os
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
|
|
||||||
|
public struct CertificateMigrator {
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.migration", category: "CertificateKitMigrator")
|
||||||
|
private let publicKeysDirectory: URL
|
||||||
|
private let certificatesDirectory: URL
|
||||||
|
private let certificateStore: CertificateStore
|
||||||
|
|
||||||
|
/// Initializes a PublicKeyFileStoreController.
|
||||||
|
public init(homeDirectory: URL, certificateStore: CertificateStore) {
|
||||||
|
publicKeysDirectory = homeDirectory.appending(component: "PublicKeys")
|
||||||
|
certificatesDirectory = homeDirectory.appending(component: "Certificates")
|
||||||
|
self.certificateStore = certificateStore
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor public func migrate() throws {
|
||||||
|
try migrate(directory: publicKeysDirectory)
|
||||||
|
try migrate(directory: certificatesDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor public func migrate(directory: URL) throws {
|
||||||
|
let fileCerts = try FileManager.default
|
||||||
|
.contentsOfDirectory(atPath: directory.path())
|
||||||
|
.filter { $0.hasSuffix("-cert.pub") }
|
||||||
|
Task {
|
||||||
|
for path in fileCerts {
|
||||||
|
do {
|
||||||
|
let url = directory.appending(component: path)
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
let parser = try await XPCCertificateParser()
|
||||||
|
let cert = try await parser.parse(data: data)
|
||||||
|
try certificateStore.save(certificate: Certificate(openSSHCertificate: cert, rawData: data))
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to delete successfully migrated cert: \(path)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to migrate cert: \(path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Foundation
|
||||||
|
import OSLog
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
|
import XPCWrappers
|
||||||
|
|
||||||
|
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
|
||||||
|
public final class XPCCertificateParser: OpenSSHCertificateParserProtocol {
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "XPCCertificateParser")
|
||||||
|
private let session: XPCTypedSession<OpenSSHCertificate, OpenSSHCertificateError>
|
||||||
|
|
||||||
|
public init() async throws {
|
||||||
|
logger.debug("Creating XPCCertificateParser")
|
||||||
|
session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretiveCertificateParser", warmup: true)
|
||||||
|
logger.debug("XPCCertificateParser is warmed up.")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func parse(data: Data) async throws -> OpenSSHCertificate {
|
||||||
|
logger.debug("Parsing input")
|
||||||
|
defer { logger.debug("Parsed input") }
|
||||||
|
return try await session.send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
session.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ extension ProcessInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let value = SecTaskCopyValueForEntitlement(task, "com.apple.developer.team-identifier" as CFString, nil) as? String else {
|
guard let value = SecTaskCopyValueForEntitlement(task, "com.apple.developer.team-identifier" as CFString, nil) as? String else {
|
||||||
assertionFailure("SecTaskCopyValueForEntitlement(com.apple.developer.team-identifier) failed")
|
// assertionFailure("SecTaskCopyValueForEntitlement(com.apple.developer.team-identifier) failed")
|
||||||
return fallbackTeamID
|
return fallbackTeamID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import CertificateKit
|
||||||
@testable import SSHProtocolKit
|
@testable import SSHProtocolKit
|
||||||
@testable import SecretKit
|
@testable import SecretKit
|
||||||
@testable import SecretAgentKit
|
@testable import SecretAgentKit
|
||||||
|
|
||||||
@Suite struct AgentTests {
|
@Suite @MainActor struct AgentTests {
|
||||||
|
|
||||||
// MARK: Identity Listing
|
// MARK: Identity Listing
|
||||||
|
|
||||||
@Test func emptyStores() async throws {
|
@Test func emptyStores() async throws {
|
||||||
let agent = Agent(storeList: SecretStoreList())
|
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore())
|
||||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
|
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
|
||||||
let response = await agent.handle(request: request, provenance: .test)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestIdentitiesEmpty)
|
#expect(response == Constants.Responses.requestIdentitiesEmpty)
|
||||||
@@ -18,7 +19,7 @@ import CryptoKit
|
|||||||
|
|
||||||
@Test func identitiesList() async throws {
|
@Test func identitiesList() async throws {
|
||||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
let agent = Agent(storeList: list)
|
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
|
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
|
||||||
let response = await agent.handle(request: request, provenance: .test)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ import CryptoKit
|
|||||||
|
|
||||||
@Test func noMatchingIdentities() async throws {
|
@Test func noMatchingIdentities() async throws {
|
||||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
let agent = Agent(storeList: list)
|
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching)
|
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching)
|
||||||
let response = await agent.handle(request: request, provenance: .test)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestFailure)
|
#expect(response == Constants.Responses.requestFailure)
|
||||||
@@ -42,7 +43,7 @@ import CryptoKit
|
|||||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||||
guard case SSHAgent.Request.signRequest(let context) = request else { return }
|
guard case SSHAgent.Request.signRequest(let context) = request else { return }
|
||||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
let agent = Agent(storeList: list)
|
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||||
let response = await agent.handle(request: request, provenance: .test)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
let responseReader = OpenSSHReader(data: response)
|
let responseReader = OpenSSHReader(data: response)
|
||||||
let length = try responseReader.readNextBytes(as: UInt32.self)
|
let length = try responseReader.readNextBytes(as: UInt32.self)
|
||||||
@@ -77,7 +78,7 @@ import CryptoKit
|
|||||||
let witness = StubWitness(speakNow: { _,_ in
|
let witness = StubWitness(speakNow: { _,_ in
|
||||||
return true
|
return true
|
||||||
}, witness: { _, _ in })
|
}, witness: { _, _ in })
|
||||||
let agent = Agent(storeList: list, witness: witness)
|
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
|
||||||
let response = await agent.handle(request: .signRequest(.empty), provenance: .test)
|
let response = await agent.handle(request: .signRequest(.empty), provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestFailure)
|
#expect(response == Constants.Responses.requestFailure)
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,7 @@ import CryptoKit
|
|||||||
}, witness: { _, trace in
|
}, witness: { _, trace in
|
||||||
witnessed = true
|
witnessed = true
|
||||||
})
|
})
|
||||||
let agent = Agent(storeList: list, witness: witness)
|
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
|
||||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||||
_ = await agent.handle(request: request, provenance: .test)
|
_ = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(witnessed)
|
#expect(witnessed)
|
||||||
@@ -106,7 +107,7 @@ import CryptoKit
|
|||||||
}, witness: { _, trace in
|
}, witness: { _, trace in
|
||||||
witnessTrace = trace
|
witnessTrace = trace
|
||||||
})
|
})
|
||||||
let agent = Agent(storeList: list, witness: witness)
|
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
|
||||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||||
_ = await agent.handle(request: request, provenance: .test)
|
_ = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(witnessTrace == speakNowTrace)
|
#expect(witnessTrace == speakNowTrace)
|
||||||
@@ -117,9 +118,9 @@ import CryptoKit
|
|||||||
|
|
||||||
@Test func signatureException() async throws {
|
@Test func signatureException() async throws {
|
||||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
let store = await list.stores.first?.base as! Stub.Store
|
let store = list.stores.first?.base as! Stub.Store
|
||||||
store.shouldThrow = true
|
store.shouldThrow = true
|
||||||
let agent = Agent(storeList: list)
|
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||||
let response = await agent.handle(request: request, provenance: .test)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestFailure)
|
#expect(response == Constants.Responses.requestFailure)
|
||||||
@@ -128,7 +129,7 @@ import CryptoKit
|
|||||||
// MARK: Unsupported
|
// MARK: Unsupported
|
||||||
|
|
||||||
@Test func unhandledAdd() async throws {
|
@Test func unhandledAdd() async throws {
|
||||||
let agent = Agent(storeList: SecretStoreList())
|
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore())
|
||||||
let response = await agent.handle(request: .addIdentity, provenance: .test)
|
let response = await agent.handle(request: .addIdentity, provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestFailure)
|
#expect(response == Constants.Responses.requestFailure)
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ extension SigningRequestProvenance {
|
|||||||
|
|
||||||
extension AgentTests {
|
extension AgentTests {
|
||||||
|
|
||||||
@MainActor func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList {
|
func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList {
|
||||||
let store = Stub.Store()
|
let store = Stub.Store()
|
||||||
store.secrets.append(contentsOf: secrets)
|
store.secrets.append(contentsOf: secrets)
|
||||||
let storeList = SecretStoreList()
|
let storeList = SecretStoreList()
|
||||||
|
|||||||
@@ -6,8 +6,21 @@ import SmartCardSecretKit
|
|||||||
import SecretAgentKit
|
import SecretAgentKit
|
||||||
import Brief
|
import Brief
|
||||||
import Observation
|
import Observation
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
import Common
|
import Common
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
|
||||||
|
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
||||||
|
|
||||||
|
@MainActor var certificateStore: CertificateStore {
|
||||||
|
EnvironmentValues._certificateStore
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@main
|
@main
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
@@ -18,18 +31,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
try? migrator.migrate(to: cryptoKit)
|
try? migrator.migrate(to: cryptoKit)
|
||||||
list.add(store: cryptoKit)
|
list.add(store: cryptoKit)
|
||||||
list.add(store: SmartCard.Store())
|
list.add(store: SmartCard.Store())
|
||||||
|
let certsMigrator = CertificateMigrator(homeDirectory: URL.homeDirectory, certificateStore: EnvironmentValues._certificateStore)
|
||||||
|
try? certsMigrator.migrate()
|
||||||
return list
|
return list
|
||||||
}()
|
}()
|
||||||
private let updater = Updater(checkOnLaunch: true)
|
private let updater = Updater(checkOnLaunch: true)
|
||||||
private let notifier = Notifier()
|
private let notifier = Notifier()
|
||||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory)
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory)
|
||||||
private lazy var agent: Agent = {
|
@MainActor private lazy var agent: Agent = {
|
||||||
Agent(storeList: storeList, witness: notifier)
|
Agent(storeList: storeList, certificateStore: EnvironmentValues._certificateStore, witness: notifier)
|
||||||
}()
|
|
||||||
private lazy var socketController: SocketController = {
|
|
||||||
let path = URL.socketPath as String
|
|
||||||
return SocketController(path: path)
|
|
||||||
}()
|
}()
|
||||||
|
private var shutdownTask: Task<Void, Error>?
|
||||||
|
private let socketController = SocketController(.launchd("SecureListener"))
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
@@ -37,16 +50,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
Task {
|
Task {
|
||||||
for await session in socketController.sessions {
|
for await session in socketController.sessions {
|
||||||
Task {
|
Task {
|
||||||
let inputParser = try await XPCAgentInputParser()
|
|
||||||
do {
|
do {
|
||||||
|
let inputParser = try await XPCAgentInputParser()
|
||||||
for await message in session.messages {
|
for await message in session.messages {
|
||||||
let request = try await inputParser.parse(data: message)
|
let request = try await inputParser.parse(data: message)
|
||||||
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
|
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
|
||||||
try session.write(agentResponse)
|
try session.write(agentResponse)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
try session.close()
|
try? session.close()
|
||||||
}
|
}
|
||||||
|
startCountdownClock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +69,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Task {
|
||||||
|
for await _ in NotificationCenter.default.notifications(named: .certificateStoreReloaded) {
|
||||||
|
try? publicKeyFileStoreController.generateCertificates(for: EnvironmentValues._certificateStore.certificates, clear: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||||
|
try? publicKeyFileStoreController.generateCertificates(for: EnvironmentValues._certificateStore.certificates, clear: true)
|
||||||
notifier.prompt()
|
notifier.prompt()
|
||||||
_ = withObservationTracking {
|
_ = withObservationTracking {
|
||||||
updater.update
|
updater.update
|
||||||
@@ -69,5 +89,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startCountdownClock() {
|
||||||
|
// FIXME: ACCOUNT FOR STORED AUTH
|
||||||
|
logger.log("Beginning countdown clock")
|
||||||
|
shutdownTask?.cancel()
|
||||||
|
shutdownTask = Task { [logger] in
|
||||||
|
try await Task.sleep(for: .seconds(30))
|
||||||
|
logger.log("Shutting down")
|
||||||
|
await NSApplication.shared.terminate(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
47
Sources/SecretAgent/CertificateMigrator.swift
Normal file
47
Sources/SecretAgent/CertificateMigrator.swift
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
import CryptoTokenKit
|
||||||
|
import CryptoKit
|
||||||
|
import os
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
|
import SharedXPCServices
|
||||||
|
|
||||||
|
public struct CertificateMigrator {
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.migration", category: "CertificateKitMigrator")
|
||||||
|
private let directory: URL
|
||||||
|
private let certificateStore: CertificateStore
|
||||||
|
|
||||||
|
/// Initializes a PublicKeyFileStoreController.
|
||||||
|
public init(homeDirectory: URL, certificateStore: CertificateStore) {
|
||||||
|
directory = homeDirectory.appending(component: "PublicKeys")
|
||||||
|
self.certificateStore = certificateStore
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor public func migrate() throws {
|
||||||
|
let fileCerts = try FileManager.default
|
||||||
|
.contentsOfDirectory(atPath: directory.path())
|
||||||
|
.filter { $0.hasSuffix("-cert.pub") }
|
||||||
|
Task {
|
||||||
|
for path in fileCerts {
|
||||||
|
do {
|
||||||
|
let url = directory.appending(component: path)
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
let parser = try await XPCCertificateParser()
|
||||||
|
let cert = try await parser.parse(data: data)
|
||||||
|
try certificateStore.save(certificate: Certificate(openSSHCertificate: cert, rawData: data))
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to delete successfully migrated cert: \(path)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to migrate cert: \(path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>com.apple.security.hardened-process.hardened-heap</key>
|
<key>com.apple.security.hardened-process.hardened-heap</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.smartcard</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
|
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
|
||||||
<string>2</string>
|
<string>2</string>
|
||||||
|
<key>com.apple.security.smartcard</key>
|
||||||
|
<true/>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>
|
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SecretAgentKit
|
import OSLog
|
||||||
|
import SSHProtocolKit
|
||||||
import Brief
|
import Brief
|
||||||
import XPCWrappers
|
import XPCWrappers
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|||||||
@@ -21,16 +21,18 @@
|
|||||||
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
||||||
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
||||||
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
||||||
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
501421652781268000BBAA70 /* SecretAgent.app in Copy SecretAgent */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
|
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||||
|
502452F92FE2026E009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist in Copy SecretAgent plist */ = {isa = PBXBuildFile; fileRef = 502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */; };
|
||||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
||||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
|
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
|
||||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
|
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
|
||||||
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
|
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
|
||||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
||||||
|
505F5EF22FA9635700C45824 /* CertificateKit in Frameworks */ = {isa = PBXBuildFile; productRef = 505F5EF12FA9635700C45824 /* CertificateKit */; };
|
||||||
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
|
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
|
||||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
|
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
|
||||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
||||||
@@ -71,6 +73,23 @@
|
|||||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
||||||
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; };
|
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; };
|
||||||
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; };
|
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; };
|
||||||
|
50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204E82FA9D12700402380 /* CertificateDetailView.swift */; };
|
||||||
|
50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204EC2FAA997F00402380 /* CertificateListItemView.swift */; };
|
||||||
|
50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */; };
|
||||||
|
50E2051D2FAAB81C00402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
50E205282FAAB82700402380 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E205242FAAB82700402380 /* main.swift */; };
|
||||||
|
50E2052C2FAAB85000402380 /* SecretiveCertificateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2052B2FAAB85000402380 /* SecretiveCertificateParser.swift */; };
|
||||||
|
50E2052D2FAAB92000402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
50E205312FAAB95500402380 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205302FAAB95500402380 /* XPCWrappers */; };
|
||||||
|
50E205332FAAB95A00402380 /* SSHProtocolKit in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205322FAAB95A00402380 /* SSHProtocolKit */; };
|
||||||
|
50E205362FAABC6300402380 /* EditCertificateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E205352FAABC6300402380 /* EditCertificateView.swift */; };
|
||||||
|
50E205372FAABC6300402380 /* DeleteCertificateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E205342FAABC6300402380 /* DeleteCertificateView.swift */; };
|
||||||
|
50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2057F2FAB291E00402380 /* CertificateMigrator.swift */; };
|
||||||
|
50E205822FAB293B00402380 /* SharedXPCServices in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205812FAB293B00402380 /* SharedXPCServices */; };
|
||||||
|
50E205842FAB296A00402380 /* SharedXPCServices in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205832FAB296A00402380 /* SharedXPCServices */; };
|
||||||
|
50E205862FAC2EA000402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205852FAC2EA000402380 /* Formatters */; };
|
||||||
|
50E205882FAC2EAB00402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205872FAC2EAB00402380 /* Formatters */; };
|
||||||
|
50E2058A2FAC2EB600402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205892FAC2EB600402380 /* Formatters */; };
|
||||||
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; };
|
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; };
|
||||||
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; };
|
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; };
|
||||||
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
|
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
|
||||||
@@ -120,6 +139,20 @@
|
|||||||
remoteGlobalIDString = 50692E4F2E6FF9D20043C7BB;
|
remoteGlobalIDString = 50692E4F2E6FF9D20043C7BB;
|
||||||
remoteInfo = SecretAgentInputParser;
|
remoteInfo = SecretAgentInputParser;
|
||||||
};
|
};
|
||||||
|
50E2051B2FAAB81C00402380 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 50E205132FAAB81C00402380;
|
||||||
|
remoteInfo = SecretAgentCertificateParser;
|
||||||
|
};
|
||||||
|
50E2052E2FAAB92000402380 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 50E205132FAAB81C00402380;
|
||||||
|
remoteInfo = SecretiveCertificateParser;
|
||||||
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
@@ -129,6 +162,7 @@
|
|||||||
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
|
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
|
||||||
dstSubfolderSpec = 16;
|
dstSubfolderSpec = 16;
|
||||||
files = (
|
files = (
|
||||||
|
50E2051D2FAAB81C00402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */,
|
||||||
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
|
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
|
||||||
50692E5B2E6FF9D20043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
|
50692E5B2E6FF9D20043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
|
||||||
);
|
);
|
||||||
@@ -142,11 +176,23 @@
|
|||||||
dstSubfolderSpec = 16;
|
dstSubfolderSpec = 16;
|
||||||
files = (
|
files = (
|
||||||
50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
|
50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
|
||||||
|
50E2052D2FAAB92000402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */,
|
||||||
50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
|
50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
|
||||||
);
|
);
|
||||||
name = "Embed XPC Services";
|
name = "Embed XPC Services";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
502452F82FE2024D009EE753 /* Copy SecretAgent plist */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = Contents/Library/LaunchAgents;
|
||||||
|
dstSubfolderSpec = 1;
|
||||||
|
files = (
|
||||||
|
502452F92FE2026E009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist in Copy SecretAgent plist */,
|
||||||
|
);
|
||||||
|
name = "Copy SecretAgent plist";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
50617DBF23FCE4AB0099B055 /* Embed Frameworks */ = {
|
50617DBF23FCE4AB0099B055 /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -167,14 +213,15 @@
|
|||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
50C385AF240E438B00AF2719 /* CopyFiles */ = {
|
50C385AF240E438B00AF2719 /* Copy SecretAgent */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
dstPath = Contents/Library/LoginItems;
|
dstPath = Contents/Library/LoginItems;
|
||||||
dstSubfolderSpec = 1;
|
dstSubfolderSpec = 1;
|
||||||
files = (
|
files = (
|
||||||
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */,
|
501421652781268000BBAA70 /* SecretAgent.app in Copy SecretAgent */,
|
||||||
);
|
);
|
||||||
|
name = "Copy SecretAgent";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
@@ -190,6 +237,7 @@
|
|||||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.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>"; };
|
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>"; };
|
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||||
|
502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = com.maxgoedjen.Secretive.SecretAgent.plist; sourceTree = "<group>"; };
|
||||||
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.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>"; };
|
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>"; };
|
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
|
||||||
@@ -238,6 +286,17 @@
|
|||||||
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
|
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
|
||||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
||||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
|
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
|
||||||
|
50E204E82FA9D12700402380 /* CertificateDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateListItemView.swift; sourceTree = "<group>"; };
|
||||||
|
50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineInfoView.swift; sourceTree = "<group>"; };
|
||||||
|
50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveCertificateParser.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
50E205232FAAB82700402380 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
50E205242FAAB82700402380 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||||
|
50E2052A2FAAB85000402380 /* SecretiveCertificateParser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretiveCertificateParser.entitlements; sourceTree = "<group>"; };
|
||||||
|
50E2052B2FAAB85000402380 /* SecretiveCertificateParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretiveCertificateParser.swift; sourceTree = "<group>"; };
|
||||||
|
50E205342FAABC6300402380 /* DeleteCertificateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteCertificateView.swift; sourceTree = "<group>"; };
|
||||||
|
50E205352FAABC6300402380 /* EditCertificateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCertificateView.swift; sourceTree = "<group>"; };
|
||||||
|
50E2057F2FAB291E00402380 /* CertificateMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateMigrator.swift; sourceTree = "<group>"; };
|
||||||
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowBackgroundStyle.swift; sourceTree = "<group>"; };
|
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowBackgroundStyle.swift; sourceTree = "<group>"; };
|
||||||
50E4C4C22E7765DF00C73783 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
50E4C4C22E7765DF00C73783 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||||
50E4C4C72E777E4200C73783 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
50E4C4C72E777E4200C73783 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||||
@@ -250,10 +309,13 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
|
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
|
||||||
|
50E2058A2FAC2EB600402380 /* Formatters in Frameworks */,
|
||||||
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
|
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
|
||||||
501421622781262300BBAA70 /* Brief in Frameworks */,
|
501421622781262300BBAA70 /* Brief in Frameworks */,
|
||||||
|
50E205842FAB296A00402380 /* SharedXPCServices in Frameworks */,
|
||||||
5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */,
|
5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */,
|
||||||
5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */,
|
5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */,
|
||||||
|
505F5EF22FA9635700C45824 /* CertificateKit in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -261,6 +323,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
50E205862FAC2EA000402380 /* Formatters in Frameworks */,
|
||||||
50692D2D2E6FDC000043C7BB /* XPCWrappers in Frameworks */,
|
50692D2D2E6FDC000043C7BB /* XPCWrappers in Frameworks */,
|
||||||
50692D312E6FDC390043C7BB /* Brief in Frameworks */,
|
50692D312E6FDC390043C7BB /* Brief in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -282,12 +345,23 @@
|
|||||||
5003EF3D278005F300DF2006 /* Brief in Frameworks */,
|
5003EF3D278005F300DF2006 /* Brief in Frameworks */,
|
||||||
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */,
|
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */,
|
||||||
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */,
|
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */,
|
||||||
|
50E205822FAB293B00402380 /* SharedXPCServices in Frameworks */,
|
||||||
5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */,
|
5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */,
|
||||||
5003EF41278005FA00DF2006 /* SecretKit in Frameworks */,
|
5003EF41278005FA00DF2006 /* SecretKit in Frameworks */,
|
||||||
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */,
|
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
50E205112FAAB81C00402380 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
50E205882FAC2EAB00402380 /* Formatters in Frameworks */,
|
||||||
|
50E205332FAAB95A00402380 /* SSHProtocolKit in Frameworks */,
|
||||||
|
50E205312FAAB95500402380 /* XPCWrappers in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -309,10 +383,14 @@
|
|||||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
||||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
||||||
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
|
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
|
||||||
|
50E205342FAABC6300402380 /* DeleteCertificateView.swift */,
|
||||||
|
50E205352FAABC6300402380 /* EditCertificateView.swift */,
|
||||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
||||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
||||||
|
50E204E82FA9D12700402380 /* CertificateDetailView.swift */,
|
||||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||||
|
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */,
|
||||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||||
);
|
);
|
||||||
path = Secrets;
|
path = Secrets;
|
||||||
@@ -338,6 +416,7 @@
|
|||||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
||||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||||
|
50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */,
|
||||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
@@ -352,6 +431,7 @@
|
|||||||
508A58AF241E144C0069DC07 /* Config */,
|
508A58AF241E144C0069DC07 /* Config */,
|
||||||
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */,
|
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */,
|
||||||
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */,
|
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */,
|
||||||
|
50E205262FAAB82700402380 /* SecretiveCertificateParser */,
|
||||||
50617D8023FCE48E0099B055 /* Products */,
|
50617D8023FCE48E0099B055 /* Products */,
|
||||||
5099A08B240243730062B6F2 /* Frameworks */,
|
5099A08B240243730062B6F2 /* Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -364,6 +444,7 @@
|
|||||||
50A3B78A24026B7500D209EA /* SecretAgent.app */,
|
50A3B78A24026B7500D209EA /* SecretAgent.app */,
|
||||||
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */,
|
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */,
|
||||||
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */,
|
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */,
|
||||||
|
50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -378,6 +459,7 @@
|
|||||||
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
|
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
|
||||||
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
|
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
|
||||||
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
||||||
|
502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */,
|
||||||
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
|
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
|
||||||
50617D8823FCE48E0099B055 /* Preview Content */,
|
50617D8823FCE48E0099B055 /* Preview Content */,
|
||||||
);
|
);
|
||||||
@@ -462,6 +544,7 @@
|
|||||||
50020BAF24064869003D4025 /* AppDelegate.swift */,
|
50020BAF24064869003D4025 /* AppDelegate.swift */,
|
||||||
5018F54E24064786002EB505 /* Notifier.swift */,
|
5018F54E24064786002EB505 /* Notifier.swift */,
|
||||||
501578122E6C0479004A37D0 /* XPCInputParser.swift */,
|
501578122E6C0479004A37D0 /* XPCInputParser.swift */,
|
||||||
|
50E2057F2FAB291E00402380 /* CertificateMigrator.swift */,
|
||||||
50A3B79524026B7600D209EA /* Main.storyboard */,
|
50A3B79524026B7600D209EA /* Main.storyboard */,
|
||||||
50A3B79824026B7600D209EA /* Info.plist */,
|
50A3B79824026B7600D209EA /* Info.plist */,
|
||||||
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
|
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
|
||||||
@@ -479,6 +562,17 @@
|
|||||||
path = "Preview Content";
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
50E205262FAAB82700402380 /* SecretiveCertificateParser */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
50E205232FAAB82700402380 /* Info.plist */,
|
||||||
|
50E205242FAAB82700402380 /* main.swift */,
|
||||||
|
50E2052A2FAAB85000402380 /* SecretiveCertificateParser.entitlements */,
|
||||||
|
50E2052B2FAAB85000402380 /* SecretiveCertificateParser.swift */,
|
||||||
|
);
|
||||||
|
path = SecretiveCertificateParser;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -490,7 +584,8 @@
|
|||||||
50617D7C23FCE48D0099B055 /* Frameworks */,
|
50617D7C23FCE48D0099B055 /* Frameworks */,
|
||||||
50617D7D23FCE48D0099B055 /* Resources */,
|
50617D7D23FCE48D0099B055 /* Resources */,
|
||||||
50617DBF23FCE4AB0099B055 /* Embed Frameworks */,
|
50617DBF23FCE4AB0099B055 /* Embed Frameworks */,
|
||||||
50C385AF240E438B00AF2719 /* CopyFiles */,
|
50C385AF240E438B00AF2719 /* Copy SecretAgent */,
|
||||||
|
502452F82FE2024D009EE753 /* Copy SecretAgent plist */,
|
||||||
501577C92E6BC5B4004A37D0 /* Embed XPC Services */,
|
501577C92E6BC5B4004A37D0 /* Embed XPC Services */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
@@ -499,6 +594,7 @@
|
|||||||
50142167278126B500BBAA70 /* PBXTargetDependency */,
|
50142167278126B500BBAA70 /* PBXTargetDependency */,
|
||||||
50692D1C2E6FDB880043C7BB /* PBXTargetDependency */,
|
50692D1C2E6FDB880043C7BB /* PBXTargetDependency */,
|
||||||
50692E5A2E6FF9D20043C7BB /* PBXTargetDependency */,
|
50692E5A2E6FF9D20043C7BB /* PBXTargetDependency */,
|
||||||
|
50E2051C2FAAB81C00402380 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Secretive;
|
name = Secretive;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -507,6 +603,9 @@
|
|||||||
5003EF602780081600DF2006 /* SmartCardSecretKit */,
|
5003EF602780081600DF2006 /* SmartCardSecretKit */,
|
||||||
501421612781262300BBAA70 /* Brief */,
|
501421612781262300BBAA70 /* Brief */,
|
||||||
50E0145B2EDB9CDF00B121F1 /* Common */,
|
50E0145B2EDB9CDF00B121F1 /* Common */,
|
||||||
|
505F5EF12FA9635700C45824 /* CertificateKit */,
|
||||||
|
50E205832FAB296A00402380 /* SharedXPCServices */,
|
||||||
|
50E205892FAC2EB600402380 /* Formatters */,
|
||||||
);
|
);
|
||||||
productName = Secretive;
|
productName = Secretive;
|
||||||
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
|
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
|
||||||
@@ -528,6 +627,7 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
50692D2C2E6FDC000043C7BB /* XPCWrappers */,
|
50692D2C2E6FDC000043C7BB /* XPCWrappers */,
|
||||||
50692D302E6FDC390043C7BB /* Brief */,
|
50692D302E6FDC390043C7BB /* Brief */,
|
||||||
|
50E205852FAC2EA000402380 /* Formatters */,
|
||||||
);
|
);
|
||||||
productName = SecretiveUpdater;
|
productName = SecretiveUpdater;
|
||||||
productReference = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */;
|
productReference = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */;
|
||||||
@@ -570,6 +670,7 @@
|
|||||||
501577D42E6BC5DD004A37D0 /* PBXTargetDependency */,
|
501577D42E6BC5DD004A37D0 /* PBXTargetDependency */,
|
||||||
50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */,
|
50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */,
|
||||||
50692E722E6FFA6E0043C7BB /* PBXTargetDependency */,
|
50692E722E6FFA6E0043C7BB /* PBXTargetDependency */,
|
||||||
|
50E2052F2FAAB92000402380 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = SecretAgent;
|
name = SecretAgent;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -579,11 +680,34 @@
|
|||||||
5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */,
|
5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */,
|
||||||
5003EF642780081B00DF2006 /* SmartCardSecretKit */,
|
5003EF642780081B00DF2006 /* SmartCardSecretKit */,
|
||||||
50E0145D2EDB9CE400B121F1 /* Common */,
|
50E0145D2EDB9CE400B121F1 /* Common */,
|
||||||
|
50E205812FAB293B00402380 /* SharedXPCServices */,
|
||||||
);
|
);
|
||||||
productName = SecretAgent;
|
productName = SecretAgent;
|
||||||
productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */;
|
productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
50E205132FAAB81C00402380 /* SecretiveCertificateParser */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 50E2051F2FAAB81C00402380 /* Build configuration list for PBXNativeTarget "SecretiveCertificateParser" */;
|
||||||
|
buildPhases = (
|
||||||
|
50E205102FAAB81C00402380 /* Sources */,
|
||||||
|
50E205112FAAB81C00402380 /* Frameworks */,
|
||||||
|
50E205122FAAB81C00402380 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = SecretiveCertificateParser;
|
||||||
|
packageProductDependencies = (
|
||||||
|
50E205302FAAB95500402380 /* XPCWrappers */,
|
||||||
|
50E205322FAAB95A00402380 /* SSHProtocolKit */,
|
||||||
|
50E205872FAC2EAB00402380 /* Formatters */,
|
||||||
|
);
|
||||||
|
productName = SecretAgentCertificateParser;
|
||||||
|
productReference = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */;
|
||||||
|
productType = "com.apple.product-type.xpc-service";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -591,7 +715,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 2600;
|
LastSwiftUpdateCheck = 2650;
|
||||||
LastUpgradeCheck = 2640;
|
LastUpgradeCheck = 2640;
|
||||||
ORGANIZATIONNAME = "Max Goedjen";
|
ORGANIZATIONNAME = "Max Goedjen";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -607,6 +731,9 @@
|
|||||||
50A3B78924026B7500D209EA = {
|
50A3B78924026B7500D209EA = {
|
||||||
CreatedOnToolsVersion = 11.4;
|
CreatedOnToolsVersion = 11.4;
|
||||||
};
|
};
|
||||||
|
50E205132FAAB81C00402380 = {
|
||||||
|
CreatedOnToolsVersion = 26.5;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 50617D7A23FCE48D0099B055 /* Build configuration list for PBXProject "Secretive" */;
|
buildConfigurationList = 50617D7A23FCE48D0099B055 /* Build configuration list for PBXProject "Secretive" */;
|
||||||
@@ -634,6 +761,7 @@
|
|||||||
50A3B78924026B7500D209EA /* SecretAgent */,
|
50A3B78924026B7500D209EA /* SecretAgent */,
|
||||||
50692D112E6FDB880043C7BB /* SecretiveUpdater */,
|
50692D112E6FDB880043C7BB /* SecretiveUpdater */,
|
||||||
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
|
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
|
||||||
|
50E205132FAAB81C00402380 /* SecretiveCertificateParser */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -677,6 +805,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
50E205122FAAB81C00402380 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -689,13 +824,16 @@
|
|||||||
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */,
|
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */,
|
||||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
||||||
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */,
|
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */,
|
||||||
|
50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */,
|
||||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||||
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
|
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
|
||||||
|
50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */,
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
||||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
|
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
|
||||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
|
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
|
||||||
|
50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */,
|
||||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
||||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
||||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
|
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
|
||||||
@@ -708,12 +846,14 @@
|
|||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
||||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
|
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
|
||||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||||
|
50E205372FAABC6300402380 /* DeleteCertificateView.swift in Sources */,
|
||||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
|
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
|
||||||
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
||||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
|
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
|
||||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
||||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
||||||
|
50E205362FAABC6300402380 /* EditCertificateView.swift in Sources */,
|
||||||
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
|
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -740,12 +880,22 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */,
|
||||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
||||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
|
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
50E205102FAAB81C00402380 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
50E205282FAAB82700402380 /* main.swift in Sources */,
|
||||||
|
50E2052C2FAAB85000402380 /* SecretiveCertificateParser.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
@@ -778,6 +928,16 @@
|
|||||||
target = 50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */;
|
target = 50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */;
|
||||||
targetProxy = 50692E712E6FFA6E0043C7BB /* PBXContainerItemProxy */;
|
targetProxy = 50692E712E6FFA6E0043C7BB /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
50E2051C2FAAB81C00402380 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 50E205132FAAB81C00402380 /* SecretiveCertificateParser */;
|
||||||
|
targetProxy = 50E2051B2FAAB81C00402380 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
50E2052F2FAAB92000402380 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 50E205132FAAB81C00402380 /* SecretiveCertificateParser */;
|
||||||
|
targetProxy = 50E2052E2FAAB92000402380 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
@@ -1379,7 +1539,7 @@
|
|||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1416,7 +1576,7 @@
|
|||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1454,7 +1614,7 @@
|
|||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1479,6 +1639,103 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
50E205202FAAB81C00402380 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_ENTITLEMENTS = SecretiveCertificateParser/SecretiveCertificateParser.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
|
||||||
|
ENABLE_APP_SANDBOX = YES;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = SecretiveCertificateParser/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = SecretiveCertificateParser;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretiveCertificateParser";
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
50E205212FAAB81C00402380 /* Test */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_ENTITLEMENTS = SecretiveCertificateParser/SecretiveCertificateParser.entitlements;
|
||||||
|
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 = SecretiveCertificateParser/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = SecretiveCertificateParser;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretiveCertificateParser";
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
50E205222FAAB81C00402380 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_ENTITLEMENTS = SecretiveCertificateParser/SecretiveCertificateParser.entitlements;
|
||||||
|
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 = SecretiveCertificateParser/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = SecretiveCertificateParser;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretiveCertificateParser";
|
||||||
|
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;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -1532,6 +1789,16 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
50E2051F2FAAB81C00402380 /* Build configuration list for PBXNativeTarget "SecretiveCertificateParser" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
50E205202FAAB81C00402380 /* Debug */,
|
||||||
|
50E205212FAAB81C00402380 /* Test */,
|
||||||
|
50E205222FAAB81C00402380 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@@ -1575,6 +1842,10 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Brief;
|
productName = Brief;
|
||||||
};
|
};
|
||||||
|
505F5EF12FA9635700C45824 /* CertificateKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = CertificateKit;
|
||||||
|
};
|
||||||
50692D2C2E6FDC000043C7BB /* XPCWrappers */ = {
|
50692D2C2E6FDC000043C7BB /* XPCWrappers */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = XPCWrappers;
|
productName = XPCWrappers;
|
||||||
@@ -1595,6 +1866,34 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Common;
|
productName = Common;
|
||||||
};
|
};
|
||||||
|
50E205302FAAB95500402380 /* XPCWrappers */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = XPCWrappers;
|
||||||
|
};
|
||||||
|
50E205322FAAB95A00402380 /* SSHProtocolKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = SSHProtocolKit;
|
||||||
|
};
|
||||||
|
50E205812FAB293B00402380 /* SharedXPCServices */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = SharedXPCServices;
|
||||||
|
};
|
||||||
|
50E205832FAB296A00402380 /* SharedXPCServices */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = SharedXPCServices;
|
||||||
|
};
|
||||||
|
50E205852FAC2EA000402380 /* Formatters */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Formatters;
|
||||||
|
};
|
||||||
|
50E205872FAC2EAB00402380 /* Formatters */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Formatters;
|
||||||
|
};
|
||||||
|
50E205892FAC2EB600402380 /* Formatters */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Formatters;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 50617D7723FCE48D0099B055 /* Project object */;
|
rootObject = 50617D7723FCE48D0099B055 /* Project object */;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "1"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
buildConfiguration = "Test"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
@@ -87,6 +87,9 @@
|
|||||||
ReferencedContainer = "container:Secretive.xcodeproj">
|
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<MetalAPIValidationSettings
|
||||||
|
isEnabled = "No">
|
||||||
|
</MetalAPIValidationSettings>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
|||||||
@@ -1,31 +1,40 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
@unsafe @preconcurrency import ServiceManagement
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import SecureEnclaveSecretKit
|
import SecureEnclaveSecretKit
|
||||||
import SmartCardSecretKit
|
import SmartCardSecretKit
|
||||||
import Brief
|
import Brief
|
||||||
|
import CertificateKit
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
final class LaunchService: Sendable {
|
||||||
|
private let service = SMAppService.agent(plistName: "com.maxgoedjen.Secretive.SecretAgent.plist")
|
||||||
|
var status: SMAppService.Status {
|
||||||
|
service.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure() {
|
||||||
|
try? service.unregister()
|
||||||
|
try! service.register()
|
||||||
|
}
|
||||||
|
|
||||||
|
func disable() {
|
||||||
|
try? service.unregister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Secretive: App {
|
struct Secretive: App {
|
||||||
|
|
||||||
@Environment(\.agentLaunchController) var agentLaunchController
|
|
||||||
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
||||||
|
|
||||||
@SceneBuilder var body: some Scene {
|
@SceneBuilder var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
.environment(EnvironmentValues._secretStoreList)
|
.environment(EnvironmentValues._secretStoreList)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
.environment(EnvironmentValues._certificateStore)
|
||||||
Task {
|
.onAppear {
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
EnvironmentValues._launchService.configure()
|
||||||
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
|
||||||
guard hasRunSetup && !explicitlyDisabled else { return }
|
|
||||||
agentLaunchController.check()
|
|
||||||
guard !agentLaunchController.developmentBuild else { return }
|
|
||||||
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
|
|
||||||
// Relaunch the agent, since it'll be running from earlier update still
|
|
||||||
try await agentLaunchController.forceLaunch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.commands {
|
.commands {
|
||||||
@@ -92,15 +101,18 @@ extension EnvironmentValues {
|
|||||||
@MainActor fileprivate static let _secretStoreList: SecretStoreList = {
|
@MainActor fileprivate static let _secretStoreList: SecretStoreList = {
|
||||||
let list = SecretStoreList()
|
let list = SecretStoreList()
|
||||||
let cryptoKit = SecureEnclave.Store()
|
let cryptoKit = SecureEnclave.Store()
|
||||||
let migrator = SecureEnclave.CryptoKitMigrator()
|
let cryptoKitMigrator = SecureEnclave.CryptoKitMigrator()
|
||||||
try? migrator.migrate(to: cryptoKit)
|
try? cryptoKitMigrator.migrate(to: cryptoKit)
|
||||||
list.add(store: cryptoKit)
|
list.add(store: cryptoKit)
|
||||||
list.add(store: SmartCard.Store())
|
list.add(store: SmartCard.Store())
|
||||||
return list
|
return list
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
||||||
|
|
||||||
private static let _agentLaunchController = AgentLaunchController()
|
private static let _agentLaunchController = AgentLaunchController()
|
||||||
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
||||||
|
|
||||||
private static let _updater: any UpdaterProtocol = {
|
private static let _updater: any UpdaterProtocol = {
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||||
return Updater(checkOnLaunch: hasRunSetup)
|
return Updater(checkOnLaunch: hasRunSetup)
|
||||||
@@ -110,9 +122,16 @@ extension EnvironmentValues {
|
|||||||
private static let _justUpdatedChecker = JustUpdatedChecker()
|
private static let _justUpdatedChecker = JustUpdatedChecker()
|
||||||
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
|
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
|
||||||
|
|
||||||
|
fileprivate static let _launchService = LaunchService()
|
||||||
|
@Entry var launchService: LaunchService = _launchService
|
||||||
|
|
||||||
@MainActor var secretStoreList: SecretStoreList {
|
@MainActor var secretStoreList: SecretStoreList {
|
||||||
EnvironmentValues._secretStoreList
|
EnvironmentValues._secretStoreList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor var certificateStore: CertificateStore {
|
||||||
|
EnvironmentValues._certificateStore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FocusedValues {
|
extension FocusedValues {
|
||||||
|
|||||||
93
Sources/Secretive/Views/Secrets/CertificateDetailView.swift
Normal file
93
Sources/Secretive/Views/Secrets/CertificateDetailView.swift
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SecretKit
|
||||||
|
import Common
|
||||||
|
import CertificateKit
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CryptoKit
|
||||||
|
struct CertificateDetailView: View {
|
||||||
|
|
||||||
|
let certificate: Certificate
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
CopyableView(
|
||||||
|
title: .certificateDetailKeyIdLabel,
|
||||||
|
image: Image(systemName: "person.text.rectangle"),
|
||||||
|
text: certificate.keyID
|
||||||
|
)
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
CopyableView(
|
||||||
|
title: .certificateDetailSerialLabel,
|
||||||
|
image: Image(systemName: "number.circle"),
|
||||||
|
text: certificate.serial.formatted()
|
||||||
|
)
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
CopyableView(
|
||||||
|
title: .secretDetailSha256FingerprintLabel,
|
||||||
|
image: Image(systemName: "touchid"),
|
||||||
|
text: OpenSSHCertificateWriter().openSSHSHA256KeyFingerprint(publicKey: certificate.publicKey)
|
||||||
|
)
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
CopyableView(
|
||||||
|
title: .secretDetailSha256FingerprintLabel,
|
||||||
|
image: Image(systemName: "touchid"),
|
||||||
|
text: OpenSSHCertificateWriter().openSSHSHA256KeyFingerprint(publicKey: certificate.signingKey)
|
||||||
|
)
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
CopyableView(
|
||||||
|
title: .certificateDetailPathLabel,
|
||||||
|
image: Image(systemName: "checkmark.seal.text.page"),
|
||||||
|
text: URL.certificatePath(for: certificate.id, in: URL.certificatesDirectory),
|
||||||
|
showRevealInFinder: true
|
||||||
|
)
|
||||||
|
if let validityRange = certificate.validityRange {
|
||||||
|
let epoch = Date(timeIntervalSince1970: 0)
|
||||||
|
let end = Date(timeIntervalSince1970: TimeInterval(UInt64.max))
|
||||||
|
switch (validityRange.lowerBound, validityRange.upperBound) {
|
||||||
|
case (epoch, end):
|
||||||
|
EmptyView()
|
||||||
|
case (epoch, let otherEnd):
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
MultilineInfoView(title: .certificateDetailValidUntilLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherEnd.formatted()])
|
||||||
|
case (let otherStart, end):
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
MultilineInfoView(title: .certificateDetailValidAfterLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherStart.formatted()])
|
||||||
|
default:
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
MultilineInfoView(title: .certificateDetailValidityRangeLabel, image: Image(systemName: "calendar.badge.clock"), items: [validityRange.formatted()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !certificate.principals.isEmpty {
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals)
|
||||||
|
}
|
||||||
|
if !certificate.criticalOptions.isEmpty {
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
MultilineInfoView(title: .certificateDetailCriticalOptionsLabel, image: Image(systemName: "person.2"), items: certificate.criticalOptions)
|
||||||
|
}
|
||||||
|
if !certificate.extensions.isEmpty {
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
MultilineInfoView(title: .certificateDetailExtensionsLabel, image: Image(systemName: "person.2"), items: certificate.extensions)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.frame(minHeight: 200, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CertificateKit
|
||||||
|
import SSHProtocolKit
|
||||||
|
|
||||||
|
struct CertificateListItemView: View {
|
||||||
|
|
||||||
|
@Environment(\.certificateStore) private var store
|
||||||
|
|
||||||
|
var certificate: Certificate
|
||||||
|
|
||||||
|
@State var isDeleting: Bool = false
|
||||||
|
@State var isRenaming: Bool = false
|
||||||
|
|
||||||
|
var deletedCertificate: (Certificate) -> Void
|
||||||
|
var renamedCertificate: (Certificate) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink(value: certificate) {
|
||||||
|
Text(certificate.name)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $isRenaming, onDismiss: {
|
||||||
|
renamedCertificate(certificate)
|
||||||
|
}, content: {
|
||||||
|
EditCertificateView(store: store, certificate: certificate)
|
||||||
|
})
|
||||||
|
.showingDeleteConfirmation(isPresented: $isDeleting, certificate, store) { deleted in
|
||||||
|
if deleted {
|
||||||
|
deletedCertificate(certificate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button(action: { isRenaming = true }) {
|
||||||
|
Image(systemName: "pencil")
|
||||||
|
Text(.secretListEditButton)
|
||||||
|
}
|
||||||
|
Button(action: { isDeleting = true }) {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
Text(.secretListDeleteButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Sources/Secretive/Views/Secrets/DeleteCertificateView.swift
Normal file
52
Sources/Secretive/Views/Secrets/DeleteCertificateView.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CertificateKit
|
||||||
|
import SSHProtocolKit
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
func showingDeleteConfirmation(isPresented: Binding<Bool>, _ certificate: Certificate, _ store: CertificateStore, dismissalBlock: @escaping (Bool) -> ()) -> some View {
|
||||||
|
modifier(DeleteCertificateConfirmationModifier(isPresented: isPresented, certificate: certificate, store: store, dismissalBlock: dismissalBlock))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeleteCertificateConfirmationModifier: ViewModifier {
|
||||||
|
|
||||||
|
var isPresented: Binding<Bool>
|
||||||
|
var certificate: Certificate
|
||||||
|
var store: CertificateStore
|
||||||
|
var dismissalBlock: (Bool) -> ()
|
||||||
|
@State var confirmedSecretName = ""
|
||||||
|
@State private var errorText: String?
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.confirmationDialog(
|
||||||
|
String(localized: .deleteConfirmationTitle(name: certificate.name)),
|
||||||
|
isPresented: isPresented,
|
||||||
|
titleVisibility: .visible,
|
||||||
|
actions: {
|
||||||
|
Button(.deleteConfirmationDeleteButton, action: delete)
|
||||||
|
Button(.deleteConfirmationCancelButton, role: .cancel) {
|
||||||
|
dismissalBlock(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.dialogIcon(Image(systemName: "lock.trianglebadge.exclamationmark.fill"))
|
||||||
|
.onExitCommand {
|
||||||
|
dismissalBlock(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try store.delete(certificate: certificate)
|
||||||
|
dismissalBlock(true)
|
||||||
|
} catch {
|
||||||
|
errorText = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier {
|
|||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
.deleteConfirmationTitle(secretName: secret.name),
|
.deleteConfirmationTitle(name: secret.name),
|
||||||
isPresented: isPresented,
|
isPresented: isPresented,
|
||||||
titleVisibility: .visible,
|
titleVisibility: .visible,
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
60
Sources/Secretive/Views/Secrets/EditCertificateView.swift
Normal file
60
Sources/Secretive/Views/Secrets/EditCertificateView.swift
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
|
|
||||||
|
struct EditCertificateView: View {
|
||||||
|
|
||||||
|
let store: CertificateStore
|
||||||
|
let certificate: Certificate
|
||||||
|
|
||||||
|
@State private var name: String
|
||||||
|
@State var errorText: String?
|
||||||
|
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
init(store: CertificateStore, certificate: Certificate) {
|
||||||
|
self.store = store
|
||||||
|
self.certificate = certificate
|
||||||
|
name = certificate.name
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
TextField(String(localized: .renameCertificateLabel), text: $name, prompt: Text(.renameCertificateNamePlaceholder))
|
||||||
|
} footer: {
|
||||||
|
if let errorText {
|
||||||
|
Text(verbatim: errorText)
|
||||||
|
.errorStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Button(.editCancelButton) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
Button(.editSaveButton, action: rename)
|
||||||
|
.disabled(name.isEmpty)
|
||||||
|
.keyboardShortcut(.return)
|
||||||
|
.primaryButton()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rename() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
var updated = certificate
|
||||||
|
updated.openSSHCertificate.name = name
|
||||||
|
try store.update(certificate: updated)
|
||||||
|
dismiss()
|
||||||
|
} catch {
|
||||||
|
errorText = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import Common
|
import Common
|
||||||
|
import CertificateKit
|
||||||
import SSHProtocolKit
|
import SSHProtocolKit
|
||||||
|
|
||||||
struct SecretDetailView<SecretType: Secret>: View {
|
struct SecretDetailView<SecretType: Secret>: View {
|
||||||
|
|
||||||
let secret: SecretType
|
let secret: SecretType
|
||||||
|
let certificates: [Certificate]
|
||||||
|
let navigateToCertificate: ((Certificate) -> Void)?
|
||||||
|
|
||||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||||
|
|
||||||
@@ -13,16 +16,42 @@ struct SecretDetailView<SecretType: Secret>: View {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret))
|
CopyableView(
|
||||||
|
title: .secretDetailSha256FingerprintLabel,
|
||||||
|
image: Image(systemName: "touchid"),
|
||||||
|
text: keyWriter.openSSHSHA256Fingerprint(secret: secret)
|
||||||
|
)
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 20)
|
.frame(height: 20)
|
||||||
CopyableView(title: .secretDetailMd5FingerprintLabel, image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret))
|
CopyableView(
|
||||||
|
title: .secretDetailMd5FingerprintLabel,
|
||||||
|
image: Image(systemName: "touchid"),
|
||||||
|
text: keyWriter.openSSHMD5Fingerprint(secret: secret)
|
||||||
|
)
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 20)
|
.frame(height: 20)
|
||||||
CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString)
|
CopyableView(
|
||||||
Spacer()
|
title: .secretDetailPublicKeyPathLabel,
|
||||||
.frame(height: 20)
|
image: Image(systemName: "lock.doc"),
|
||||||
CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory), showRevealInFinder: true)
|
text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory),
|
||||||
|
showRevealInFinder: true
|
||||||
|
)
|
||||||
|
if !certificates.isEmpty {
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 20)
|
||||||
|
MultilineInfoView(
|
||||||
|
title: .secretDetailCertificatePathLabel,
|
||||||
|
image: Image(
|
||||||
|
systemName: "checkmark.seal.text.page"
|
||||||
|
),
|
||||||
|
items: certificates.map({ certificate in
|
||||||
|
MultilineInfoView.Item(
|
||||||
|
text: certificate.name,
|
||||||
|
action: (Image(systemName: "chevron.forward"), { navigateToCertificate?(certificate) })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,10 +61,6 @@ struct SecretDetailView<SecretType: Secret>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var keyString: String {
|
|
||||||
keyWriter.openSSHString(secret: secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#Preview {
|
//#Preview {
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
|
|
||||||
struct StoreListView: View {
|
struct StoreListView: View {
|
||||||
|
|
||||||
@Binding var activeSecret: AnySecret?
|
enum StoreListSelection: Hashable {
|
||||||
|
case secret(AnySecret)
|
||||||
|
case certificate(Certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binding var selection: StoreListSelection?
|
||||||
|
|
||||||
@Environment(\.secretStoreList) private var storeList
|
@Environment(\.secretStoreList) private var storeList
|
||||||
|
@Environment(\.certificateStore) private var certificateStore
|
||||||
|
|
||||||
private func secretDeleted(secret: AnySecret) {
|
private func secretDeleted(secret: AnySecret) {
|
||||||
activeSecret = nextDefaultSecret
|
selection = nextDefaultSecret.map(StoreListSelection.secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func secretRenamed(secret: AnySecret) {
|
private func secretRenamed(secret: AnySecret) {
|
||||||
// Pull new version from store, so we get all updated attributes
|
// Pull new version from store, so we get all updated attributes
|
||||||
activeSecret = nil
|
selection = nil
|
||||||
activeSecret = storeList.allSecrets.first(where: { $0.id == secret.id })
|
selection = storeList.allSecrets.first(where: { $0.id == secret.id }).map(StoreListSelection.secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationSplitView {
|
NavigationSplitView {
|
||||||
List(selection: $activeSecret) {
|
List(selection: $selection) {
|
||||||
ForEach(storeList.stores) { store in
|
ForEach(storeList.stores) { store in
|
||||||
if store.isAvailable {
|
if store.isAvailable {
|
||||||
Section(header: Text(store.name)) {
|
Section(header: Text(store.name)) {
|
||||||
@@ -30,29 +38,51 @@ struct StoreListView: View {
|
|||||||
deletedSecret: secretDeleted,
|
deletedSecret: secretDeleted,
|
||||||
renamedSecret: secretRenamed,
|
renamedSecret: secretRenamed,
|
||||||
)
|
)
|
||||||
|
.tag(StoreListSelection.secret(secret))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !certificateStore.certificates.isEmpty {
|
||||||
|
Section("Certificates") {
|
||||||
|
ForEach(certificateStore.certificates) { certificate in
|
||||||
|
CertificateListItemView(
|
||||||
|
certificate: certificate,
|
||||||
|
deletedCertificate: { _ in },
|
||||||
|
renamedCertificate: { _ in }
|
||||||
|
)
|
||||||
|
.tag(StoreListSelection.certificate(certificate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} detail: {
|
} detail: {
|
||||||
if let activeSecret {
|
switch selection {
|
||||||
SecretDetailView(secret: activeSecret)
|
case .secret(let secret):
|
||||||
} else if let nextDefaultSecret {
|
SecretDetailView(secret: secret, certificates: certificateStore.certificates(for: secret)) {
|
||||||
// This just means onAppear hasn't executed yet.
|
selection = .certificate($0)
|
||||||
// Do this to avoid a blip.
|
}
|
||||||
SecretDetailView(secret: nextDefaultSecret)
|
case .certificate(let certificate):
|
||||||
} else {
|
CertificateDetailView(certificate: certificate)
|
||||||
if let modifiable = storeList.modifiableStore, modifiable.isAvailable {
|
case nil:
|
||||||
EmptyStoreView(store: modifiable)
|
if let nextDefaultSecret {
|
||||||
|
// This just means onAppear hasn't executed yet.
|
||||||
|
// Do this to avoid a blip.
|
||||||
|
SecretDetailView(secret: nextDefaultSecret, certificates: certificateStore.certificates(for: nextDefaultSecret)) {
|
||||||
|
selection = .certificate($0)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
EmptyStoreView(store: storeList.stores.first(where: \.isAvailable))
|
if let modifiable = storeList.modifiableStore, modifiable.isAvailable {
|
||||||
|
EmptyStoreView(store: modifiable)
|
||||||
|
} else {
|
||||||
|
EmptyStoreView(store: storeList.stores.first(where: \.isAvailable))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationSplitViewStyle(.balanced)
|
.navigationSplitViewStyle(.balanced)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
activeSecret = nextDefaultSecret
|
selection = nextDefaultSecret.map(StoreListSelection.secret)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 100, idealWidth: 240)
|
.frame(minWidth: 100, idealWidth: 240)
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import SwiftUI
|
|||||||
|
|
||||||
struct AgentStatusView: View {
|
struct AgentStatusView: View {
|
||||||
|
|
||||||
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
|
@Environment(\.launchService) private var launchService
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if agentLaunchController.running {
|
if launchService.status == .enabled {
|
||||||
AgentRunningView()
|
AgentRunningView()
|
||||||
} else {
|
} else {
|
||||||
AgentNotRunningView()
|
AgentNotRunningView()
|
||||||
@@ -14,54 +14,53 @@ struct AgentStatusView: View {
|
|||||||
}
|
}
|
||||||
struct AgentRunningView: View {
|
struct AgentRunningView: View {
|
||||||
|
|
||||||
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
|
@Environment(\.launchService) private var launchService
|
||||||
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
if let process = agentLaunchController.process {
|
// if let process = agentLaunchController.process {
|
||||||
ConfigurationItemView(
|
// ConfigurationItemView(
|
||||||
title: .agentDetailsLocationTitle,
|
// title: .agentDetailsLocationTitle,
|
||||||
value: process.bundleURL!.path(),
|
// value: process.bundleURL!.path(),
|
||||||
action: .revealInFinder(process.bundleURL!.path()),
|
// action: .revealInFinder(process.bundleURL!.path()),
|
||||||
)
|
// )
|
||||||
ConfigurationItemView(
|
ConfigurationItemView(
|
||||||
title: .agentDetailsSocketPathTitle,
|
title: .agentDetailsSocketPathTitle,
|
||||||
value: URL.socketPath,
|
value: URL.socketPath,
|
||||||
action: .copy(URL.socketPath),
|
action: .copy(URL.socketPath),
|
||||||
)
|
)
|
||||||
ConfigurationItemView(
|
// ConfigurationItemView(
|
||||||
title: .agentDetailsVersionTitle,
|
// title: .agentDetailsVersionTitle,
|
||||||
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
|
// value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
|
||||||
)
|
// )
|
||||||
if let launchDate = process.launchDate {
|
// if let launchDate = process.launchDate {
|
||||||
ConfigurationItemView(
|
// ConfigurationItemView(
|
||||||
title: .agentDetailsRunningSinceTitle,
|
// title: .agentDetailsRunningSinceTitle,
|
||||||
value: launchDate.formatted()
|
// value: launchDate.formatted()
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} header: {
|
} header: {
|
||||||
Text(.agentRunningNoticeDetailTitle)
|
Text(.agentReadyNoticeDetailTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
} footer: {
|
} footer: {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(.agentRunningNoticeDetailDescription)
|
Text(.agentReadyNoticeDetailDescription)
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Menu(.agentDetailsRestartAgentButton) {
|
Menu(.agentDetailsRestartAgentButton) {
|
||||||
Button(.agentDetailsDisableAgentButton) {
|
Button(.agentDetailsDisableAgentButton) {
|
||||||
Task {
|
Task {
|
||||||
explicitlyDisabled = true
|
explicitlyDisabled = true
|
||||||
try? await agentLaunchController
|
launchService.disable()
|
||||||
.uninstall()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} primaryAction: {
|
} primaryAction: {
|
||||||
Task {
|
Task {
|
||||||
try? await agentLaunchController.forceLaunch()
|
launchService.configure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +77,6 @@ struct AgentRunningView: View {
|
|||||||
|
|
||||||
struct AgentNotRunningView: View {
|
struct AgentNotRunningView: View {
|
||||||
|
|
||||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
|
||||||
@State var triedRestart = false
|
@State var triedRestart = false
|
||||||
@State var loading = false
|
@State var loading = false
|
||||||
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
||||||
@@ -87,12 +85,12 @@ struct AgentNotRunningView: View {
|
|||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
} header: {
|
} header: {
|
||||||
Text(.agentNotRunningNoticeTitle)
|
Text(.agentNotConfiguredNoticeTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
} footer: {
|
} footer: {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(.agentNotRunningNoticeDetailDescription)
|
Text(.agentNotConfiguredNoticeDetailDescription)
|
||||||
HStack {
|
HStack {
|
||||||
if !triedRestart {
|
if !triedRestart {
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -100,14 +98,6 @@ struct AgentNotRunningView: View {
|
|||||||
explicitlyDisabled = false
|
explicitlyDisabled = false
|
||||||
guard !loading else { return }
|
guard !loading else { return }
|
||||||
loading = true
|
loading = true
|
||||||
Task {
|
|
||||||
try await agentLaunchController.forceLaunch()
|
|
||||||
loading = false
|
|
||||||
|
|
||||||
if !agentLaunchController.running {
|
|
||||||
triedRestart = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
if !loading {
|
if !loading {
|
||||||
Text(.agentDetailsStartAgentButton)
|
Text(.agentDetailsStartAgentButton)
|
||||||
|
|||||||
@@ -3,18 +3,23 @@ import SecretKit
|
|||||||
import SecureEnclaveSecretKit
|
import SecureEnclaveSecretKit
|
||||||
import SmartCardSecretKit
|
import SmartCardSecretKit
|
||||||
import Brief
|
import Brief
|
||||||
|
import SSHProtocolKit
|
||||||
|
import SharedXPCServices
|
||||||
|
import CertificateKit
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
@State var activeSecret: AnySecret?
|
@State var selection: StoreListView.StoreListSelection?
|
||||||
|
|
||||||
@State private var selectedUpdate: Release?
|
@State private var selectedUpdate: Release?
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@Environment(\.openWindow) private var openWindow
|
@Environment(\.openWindow) private var openWindow
|
||||||
@Environment(\.secretStoreList) private var storeList
|
@Environment(\.secretStoreList) private var storeList
|
||||||
|
@Environment(\.certificateStore) private var certificateStore
|
||||||
@Environment(\.updater) private var updater
|
@Environment(\.updater) private var updater
|
||||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
@Environment(\.agentLaunchController) private var agentLaunchController
|
||||||
|
@Environment(\.launchService) private var launchService
|
||||||
|
|
||||||
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
|
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
|
||||||
@State private var showingCreation = false
|
@State private var showingCreation = false
|
||||||
@@ -25,7 +30,7 @@ struct ContentView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if storeList.anyAvailable {
|
if storeList.anyAvailable {
|
||||||
StoreListView(activeSecret: $activeSecret)
|
StoreListView(selection: $selection)
|
||||||
} else {
|
} else {
|
||||||
NoStoresView()
|
NoStoresView()
|
||||||
}
|
}
|
||||||
@@ -42,6 +47,22 @@ struct ContentView: View {
|
|||||||
runningSetup = true
|
runningSetup = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dropDestination(for: URL.self) { items, location in
|
||||||
|
guard let url = items.first, url.pathExtension == "pub" else { return false }
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
let parser = try await XPCCertificateParser()
|
||||||
|
let cert = try await parser.parse(data: data)
|
||||||
|
let wrapped = Certificate(openSSHCertificate: cert, rawData: data)
|
||||||
|
try certificateStore.save(certificate: wrapped)
|
||||||
|
selection = .certificate(wrapped)
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} isTargeted: { _ in }
|
||||||
.focusedSceneValue(\.showCreateSecret, .init(isEnabled: !runningSetup) {
|
.focusedSceneValue(\.showCreateSecret, .init(isEnabled: !runningSetup) {
|
||||||
showingCreation = true
|
showingCreation = true
|
||||||
})
|
})
|
||||||
@@ -49,7 +70,7 @@ struct ContentView: View {
|
|||||||
if let modifiable = storeList.modifiableStore {
|
if let modifiable = storeList.modifiableStore {
|
||||||
CreateSecretView(store: modifiable) { created in
|
CreateSecretView(store: modifiable) { created in
|
||||||
if let created {
|
if let created {
|
||||||
activeSecret = created
|
selection = .secret(created)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,15 +148,15 @@ extension ContentView {
|
|||||||
showingAgentInfo = true
|
showingAgentInfo = true
|
||||||
}, label: {
|
}, label: {
|
||||||
HStack {
|
HStack {
|
||||||
if agentLaunchController.running {
|
if launchService.status == .enabled {
|
||||||
Text(.agentRunningNoticeTitle)
|
Text(.agentReadyNoticeTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||||
Circle()
|
Circle()
|
||||||
.frame(width: 10, height: 10)
|
.frame(width: 10, height: 10)
|
||||||
.foregroundColor(Color.green)
|
.foregroundColor(Color.green)
|
||||||
} else {
|
} else {
|
||||||
Text(.agentNotRunningNoticeTitle)
|
Text(.agentNotConfiguredNoticeTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Circle()
|
Circle()
|
||||||
.frame(width: 10, height: 10)
|
.frame(width: 10, height: 10)
|
||||||
@@ -145,8 +166,8 @@ extension ContentView {
|
|||||||
})
|
})
|
||||||
.buttonStyle(
|
.buttonStyle(
|
||||||
ToolbarStatusButtonStyle(
|
ToolbarStatusButtonStyle(
|
||||||
lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
lightColor: launchService.status == .enabled ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||||
darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
darkColor: launchService.status == .enabled ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import UniformTypeIdentifiers
|
|||||||
struct CopyableView: View {
|
struct CopyableView: View {
|
||||||
|
|
||||||
var title: LocalizedStringResource
|
var title: LocalizedStringResource
|
||||||
|
var subtitle: String?
|
||||||
var image: Image
|
var image: Image
|
||||||
var text: String
|
var text: String
|
||||||
var showRevealInFinder = false
|
var showRevealInFinder = false
|
||||||
|
|
||||||
@State private var interactionState: InteractionState = .normal
|
@State private var interactionState: InteractionState = .normal
|
||||||
|
|
||||||
var content: some View {
|
var content: some View {
|
||||||
VStack(alignment: .leading, spacing: 15) {
|
VStack(alignment: .leading, spacing: 15) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -17,9 +18,16 @@ struct CopyableView: View {
|
|||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
.imageScale(.large)
|
.imageScale(.large)
|
||||||
.foregroundColor(primaryTextColor)
|
.foregroundColor(primaryTextColor)
|
||||||
Text(title)
|
VStack(alignment: .leading) {
|
||||||
.font(.headline)
|
Text(title)
|
||||||
.foregroundColor(primaryTextColor)
|
.font(.headline)
|
||||||
|
.foregroundColor(primaryTextColor)
|
||||||
|
if let subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(.subheadline, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
if interactionState != .normal {
|
if interactionState != .normal {
|
||||||
HStack {
|
HStack {
|
||||||
|
|||||||
167
Sources/Secretive/Views/Views/MultilineInfoView.swift
Normal file
167
Sources/Secretive/Views/Views/MultilineInfoView.swift
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
struct MultilineInfoView: View {
|
||||||
|
|
||||||
|
struct Item {
|
||||||
|
let text: String
|
||||||
|
let action: (Image, () -> Void)?
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: LocalizedStringResource
|
||||||
|
var image: Image
|
||||||
|
var items: [Item]
|
||||||
|
|
||||||
|
init(title: LocalizedStringResource, image: Image, items: [Item]) {
|
||||||
|
self.title = title
|
||||||
|
self.image = image
|
||||||
|
self.items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
init(title: LocalizedStringResource, image: Image, items: [String]) {
|
||||||
|
self.title = title
|
||||||
|
self.image = image
|
||||||
|
self.items = items.map({ Item(text: $0, action: nil) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@State private var interactionState: InteractionState = .normal
|
||||||
|
@State private var interactionStateIndex: Int?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack {
|
||||||
|
image
|
||||||
|
.renderingMode(.template)
|
||||||
|
.imageScale(.large)
|
||||||
|
.foregroundColor(primaryTextColor)
|
||||||
|
Text(title)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(primaryTextColor)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.safeAreaPadding(20)
|
||||||
|
ForEach(Array(items.enumerated()), id: \.offset) { item in
|
||||||
|
Divider()
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.opacity(item.offset == 0 ? 1 : 0.75)
|
||||||
|
HStack {
|
||||||
|
Text(item.element.text)
|
||||||
|
Spacer()
|
||||||
|
if let (image, _) = item.element.action {
|
||||||
|
image
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.safeAreaPadding(20)
|
||||||
|
._background(interactionState: interactionStateIndex == item.offset ? interactionState : .normal, cornerRadius: 0)
|
||||||
|
.onHover { hovering in
|
||||||
|
withAnimation {
|
||||||
|
guard item.element.action != nil else { return }
|
||||||
|
interactionState = hovering ? .hovering : .normal
|
||||||
|
interactionStateIndex = item.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.gesture(
|
||||||
|
TapGesture()
|
||||||
|
.onEnded {
|
||||||
|
item.element.action?.1()
|
||||||
|
withAnimation {
|
||||||
|
interactionState = .normal
|
||||||
|
interactionStateIndex = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
._background(interactionState: .normal)
|
||||||
|
.frame(minWidth: 150, maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
var primaryTextColor: Color {
|
||||||
|
switch interactionState {
|
||||||
|
case .normal, .hovering:
|
||||||
|
return Color(.textColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondaryTextColor: Color {
|
||||||
|
switch interactionState {
|
||||||
|
case .normal, .hovering:
|
||||||
|
return Color(.secondaryLabelColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate enum InteractionState {
|
||||||
|
case normal, hovering
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
fileprivate func _background(interactionState: InteractionState, cornerRadius: Double = 15) -> some View {
|
||||||
|
modifier(BackgroundViewModifier(interactionState: interactionState, cornerRadius: cornerRadius))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct BackgroundViewModifier: ViewModifier {
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.appearsActive) private var appearsActive
|
||||||
|
|
||||||
|
let interactionState: InteractionState
|
||||||
|
let cornerRadius: Double
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
content
|
||||||
|
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
|
||||||
|
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
|
||||||
|
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
|
.mask(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
|
.shadow(color: .black.opacity(0.1), radius: 5)
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
.background(backgroundColor(interactionState: interactionState))
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func backgroundColor(interactionState: InteractionState) -> Color {
|
||||||
|
guard appearsActive else { return Color.clear }
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
|
||||||
|
switch interactionState {
|
||||||
|
case .normal:
|
||||||
|
return base
|
||||||
|
case .hovering:
|
||||||
|
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch interactionState {
|
||||||
|
case .normal:
|
||||||
|
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
|
||||||
|
case .hovering:
|
||||||
|
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
MultilineInfoView(title: "Multiple", image: Image(systemName: "figure.wave"), items: [
|
||||||
|
MultilineInfoView.Item(text: "hello", action: (Image(systemName: "chevron.forward"), {})),
|
||||||
|
MultilineInfoView.Item(text: "World", action: (Image(systemName: "chevron.forward"), {})),
|
||||||
|
])
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world."])
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
18
Sources/Secretive/com.maxgoedjen.Secretive.SecretAgent.plist
Normal file
18
Sources/Secretive/com.maxgoedjen.Secretive.SecretAgent.plist
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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>Label</key>
|
||||||
|
<string>com.maxgoedjen.Secretive.SecretAgent</string>
|
||||||
|
<key>BundleProgram</key>
|
||||||
|
<string>Contents/Library/LoginItems/SecretAgent.app/Contents/MacOS/SecretAgent</string>
|
||||||
|
<key>Sockets</key>
|
||||||
|
<dict>
|
||||||
|
<key>SecureListener</key>
|
||||||
|
<dict>
|
||||||
|
<key>SecureSocketWithKey</key>
|
||||||
|
<string>SECRETAGENT_SOCK</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
11
Sources/SecretiveCertificateParser/Info.plist
Normal file
11
Sources/SecretiveCertificateParser/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>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?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>com.apple.security.hardened-process</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.hardened-process.checked-allocations</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.hardened-process.checked-allocations.enable-pure-data</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.hardened-process.checked-allocations.no-tagged-receive</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.hardened-process.dyld-ro</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.hardened-process.enhanced-security-version</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>com.apple.security.hardened-process.hardened-heap</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.hardened-process.platform-restrictions</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import Foundation
|
||||||
|
import OSLog
|
||||||
|
import XPCWrappers
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
|
|
||||||
|
final class SecretiveCertificateParser: NSObject, XPCProtocol {
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.SecretiveCertificateParser", category: "SecretiveCertificateParser")
|
||||||
|
|
||||||
|
func process(_ data: Data) async throws -> OpenSSHCertificate {
|
||||||
|
let parser = OpenSSHCertificateParser()
|
||||||
|
let result = try parser.parse(data: data)
|
||||||
|
logger.log("Parser parsed certificate")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
Sources/SecretiveCertificateParser/main.swift
Normal file
7
Sources/SecretiveCertificateParser/main.swift
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Foundation
|
||||||
|
import XPCWrappers
|
||||||
|
|
||||||
|
let delegate = XPCServiceDelegate(exportedObject: SecretiveCertificateParser())
|
||||||
|
let listener = NSXPCListener.service()
|
||||||
|
listener.delegate = delegate
|
||||||
|
listener.resume()
|
||||||
Reference in New Issue
Block a user