mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-10-02 12:49:55 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da56dd6434 | ||
|
|
00804587de | ||
|
|
b71cbf8529 | ||
|
|
f527e136e3 | ||
|
|
ece3865d9a | ||
|
|
6b1f5bbb7c | ||
|
|
f848eb659e |
@@ -11,7 +11,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze (${{ matrix.language }})
|
name: Analyze (${{ matrix.language }})
|
||||||
runs-on: ${{ (matrix.language == 'swift' && 'xcode-27') || 'ubuntu-latest' }}
|
runs-on: ${{ (matrix.language == 'swift' && 'macos-26') || 'ubuntu-latest' }}
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write
|
security-events: write
|
||||||
packages: read
|
packages: read
|
||||||
@@ -24,15 +24,20 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- language: actions
|
- language: actions
|
||||||
build-mode: none
|
build-mode: none
|
||||||
|
# Disable this until CodeQL supports Xcode 26 builds.
|
||||||
|
# - language: swift
|
||||||
|
# build-mode: manual
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
- if: matrix.build-mode == 'manual'
|
- if: matrix.build-mode == 'manual'
|
||||||
|
name: "Select Xcode"
|
||||||
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.4.app
|
||||||
- if: matrix.build-mode == 'manual'
|
- if: matrix.build-mode == 'manual'
|
||||||
name: "Build"
|
name: "Build"
|
||||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: xcode-27
|
runs-on: macos-26
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
@@ -25,6 +25,8 @@ jobs:
|
|||||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
|
- name: Set Environment
|
||||||
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.4.app
|
||||||
- name: Update Build Number
|
- name: Update Build Number
|
||||||
env:
|
env:
|
||||||
RUN_ID: ${{ github.run_id }}
|
RUN_ID: ${{ github.run_id }}
|
||||||
@@ -34,7 +36,7 @@ jobs:
|
|||||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
- name: Build
|
- name: Build
|
||||||
run: IDEEnableNewPackagePIFBuilder=NO xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
- name: Move to Artifact Folder
|
- name: Move to Artifact Folder
|
||||||
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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: xcode-27
|
runs-on: macos-26
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
@@ -24,6 +24,8 @@ jobs:
|
|||||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
|
- name: Set Environment
|
||||||
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.4.app
|
||||||
- name: Update Build Number
|
- name: Update Build Number
|
||||||
env:
|
env:
|
||||||
RUN_ID: ${{ github.run_id }}
|
RUN_ID: ${{ github.run_id }}
|
||||||
@@ -33,7 +35,7 @@ jobs:
|
|||||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
- name: Build
|
- name: Build
|
||||||
run: IDEEnableNewPackagePIFBuilder=NO xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
- name: Move to Artifact Folder
|
- name: Move to Artifact Folder
|
||||||
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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
runs-on: xcode-27
|
runs-on: macos-26
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
@@ -21,6 +21,8 @@ jobs:
|
|||||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
|
- name: Set Environment
|
||||||
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.4.app
|
||||||
- name: Test
|
- name: Test
|
||||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
|
||||||
# SPM doesn't seem to pick up on the tests currently?
|
# SPM doesn't seem to pick up on the tests currently?
|
||||||
@@ -32,7 +34,7 @@ jobs:
|
|||||||
attestations: write
|
attestations: write
|
||||||
artifact-metadata: write
|
artifact-metadata: write
|
||||||
actions: read
|
actions: read
|
||||||
runs-on: xcode-27
|
runs-on: macos-26
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
@@ -45,6 +47,8 @@ jobs:
|
|||||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
|
- name: Set Environment
|
||||||
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.4.app
|
||||||
- name: Update Build Number
|
- name: Update Build Number
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ github.ref }}
|
TAG_NAME: ${{ github.ref }}
|
||||||
@@ -55,7 +59,7 @@ jobs:
|
|||||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
sed -i '' -e "s/GITHUB_BUILD_URL/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
sed -i '' -e "s/GITHUB_BUILD_URL/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||||
- name: Build
|
- name: Build
|
||||||
run: IDEEnableNewPackagePIFBuilder=NO xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
- name: Move to Artifact Folder
|
- name: Move to Artifact Folder
|
||||||
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
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
runs-on: xcode-27
|
runs-on: macos-26
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
- name: Set Environment
|
||||||
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.4.app
|
||||||
- name: Test Main Packages
|
- name: Test Main Packages
|
||||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
|
||||||
# SPM doesn't seem to pick up on the tests currently?
|
# SPM doesn't seem to pick up on the tests currently?
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "2700"
|
|
||||||
version = "1.7">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "YES"
|
|
||||||
buildImplicitDependencies = "YES"
|
|
||||||
buildArchitectures = "Automatic">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "SecretAgentKit"
|
|
||||||
BuildableName = "SecretAgentKit"
|
|
||||||
ReferencedContainer = "container:">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
shouldAutocreateTestPlan = "YES">
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
launchStyle = "0"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
allowLocationSimulation = "YES"
|
|
||||||
queueDebuggingEnabled = "No">
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES">
|
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "SecretAgentKit"
|
|
||||||
BuildableName = "SecretAgentKit"
|
|
||||||
ReferencedContainer = "container:">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version:6.4
|
// swift-tools-version:6.2
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@@ -7,7 +7,7 @@ let package = Package(
|
|||||||
name: "SecretivePackages",
|
name: "SecretivePackages",
|
||||||
defaultLocalization: "en",
|
defaultLocalization: "en",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v15)
|
.macOS(.v14)
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
@@ -22,9 +22,6 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "CertificateKit",
|
name: "CertificateKit",
|
||||||
targets: ["CertificateKit"]),
|
targets: ["CertificateKit"]),
|
||||||
.library(
|
|
||||||
name: "SettingsKit",
|
|
||||||
targets: ["SettingsKit"]),
|
|
||||||
.library(
|
.library(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
targets: ["SecretAgentKit"]),
|
targets: ["SecretAgentKit"]),
|
||||||
@@ -79,12 +76,6 @@ let package = Package(
|
|||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
.target(
|
|
||||||
name: "SettingsKit",
|
|
||||||
dependencies: [],
|
|
||||||
resources: [localization],
|
|
||||||
swiftSettings: swiftSettings,
|
|
||||||
),
|
|
||||||
.target(
|
.target(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"],
|
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"],
|
||||||
@@ -120,7 +111,7 @@ let package = Package(
|
|||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SharedXPCServices",
|
name: "SharedXPCServices",
|
||||||
dependencies: ["XPCWrappers", "CertificateKit", "SSHProtocolKit"],
|
dependencies: ["CertificateKit", "SSHProtocolKit"],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings,
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
@@ -148,7 +139,7 @@ var localization: Resource {
|
|||||||
var swiftSettings: [PackageDescription.SwiftSetting] {
|
var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||||
[
|
[
|
||||||
.swiftLanguageMode(.v6),
|
.swiftLanguageMode(.v6),
|
||||||
.treatAllWarnings(as: .error),
|
// .treatAllWarnings(as: .error),
|
||||||
.strictMemorySafety()
|
.strictMemorySafety()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,8 +490,8 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Log de Build"
|
"value" : "Build Log"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -675,8 +675,8 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Sobre o Secretive"
|
"value" : "About Secretive"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -860,8 +860,8 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "O Secretive é Open Source e distribuído sob a licença MIT"
|
"value" : "Secretive is Open Source and MIT Licensed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -1045,8 +1045,8 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Agradecimentos especiais aos nossos [Contribuidores](%1$(contributorsLink)@) e [Patrocinadores](%2$(sponsorsLink)@)"
|
"value" : "Special thanks our [Contributors](%1$(contributorsLink)@) and [Sponsors](%2$(sponsorsLink)@)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -1230,8 +1230,8 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Ver no GitHub"
|
"value" : "View on GitHub"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -1416,7 +1416,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O Secretive não foi capaz de iniciar o SecretAgent. Por favor, tente reiniciar seu Mac e, se isso não resolver, registre uma issue no GitHub."
|
"value" : "Secretive não foi capaz de obter o SecretAgent para iniciar. Por favor tente reiniciar seu Mac e se isso não resolver, registre um problema em nosso GitHub."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -1601,7 +1601,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Desativar o Agente"
|
"value" : "Desabilitar o Agente"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -2156,7 +2156,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Caminho do socket"
|
"value" : "Caminho do Socket"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -2526,7 +2526,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Iniciando Agente"
|
"value" : "Agente iniciando"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -2896,7 +2896,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O SecretAgent é um processo que roda em segundo plano para assinar pedidos, sem precisar manter o Secretive aberto o tempo todo.\n\n**O Secretive não funcionará corretamente a menos que o agente esteja instalado e rodando.**"
|
"value" : "SecretAgent é um processo que é executado em segundo plano para assinar pedidos, então não há necessidade de manter Secretive aberto o tempo todo.\n\n**Secretive não funcionará corretamente a menos que o agente esteja instalado e executando.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -3081,7 +3081,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O Agente não está rodando"
|
"value" : "Agent não está rodando"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -3266,7 +3266,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O SecretAgent é um processo que roda em segundo plano para assinar pedidos, sem precisar manter o Secretive aberto o tempo todo.\n\n**Você pode fechar o Secretive e tudo continuará funcionando.**"
|
"value" : "SecretAgent é um processo que roda em background para assinar requisições para que você não precise manter o Secretive aberto a todo momento.\n\n**Você pode fechar o Secretive e tudo continuará funcionando.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -3451,7 +3451,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O SecretAgent está rodando"
|
"value" : "Secret Agent está rodando"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -3636,7 +3636,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O Agente está rodando"
|
"value" : "Agent está rodando"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -3879,6 +3879,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"All" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Allow Connection Operations" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Allow Forwarding" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Allow Signing Operations" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Allowed Domains" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"app_menu_help_button" : {
|
"app_menu_help_button" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -4375,8 +4390,8 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Depois"
|
"value" : "Later"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -4561,7 +4576,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O Secretive precisa estar no seu diretório de Aplicativos para funcionar corretamente. Por favor, mova-o e abra novamente."
|
"value" : "Secretive necessita estar no seu diretório de Aplicações para funcionar corretamente. Por favor mova-o e abra novamente."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -4745,8 +4760,8 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Encerrar"
|
"value" : "Quit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -4931,7 +4946,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O Secretive não está no diretório de Aplicativos"
|
"value" : "Secretive não está no diretório de Aplicações"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -4990,28 +5005,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth_context_connecting_to_unknown_host" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Connecting to unknown host"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth_context_connecting_to_username_and_host" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Connecting to %1$(username)@@%2$(host)@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth_context_persist_for_duration" : {
|
"auth_context_persist_for_duration" : {
|
||||||
"comment" : "When the user clicks the notification to leave a secret unlocked, they are shown a prompt to approve the action. This is the description, showing which secret will used. The first placeholder is the name of the secret. The second placeholder is a localized description of the time period it will remain unlocked for (eg: “five minutes”)",
|
"comment" : "When the user clicks the notification to leave a secret unlocked, they are shown a prompt to approve the action. This is the description, showing which secret will used. The first placeholder is the name of the secret. The second placeholder is a localized description of the time period it will remain unlocked for (eg: “five minutes”)",
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -5139,7 +5132,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "destrancar o segredo “%1$(secretName)@” por %2$(duration)@"
|
"value" : "destravar segredo “%1$(secretName)@” for %2$(duration)@"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -5384,17 +5377,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth_context_request_multiple" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "sign multiple requests from “%1$(appName)@” using secret “%2$(secretName)@”"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth_context_request_signature_description" : {
|
"auth_context_request_signature_description" : {
|
||||||
"comment" : "When the user performs a signature action using a secret, they are shown a prompt to approve the action. This is the description, showing which secret will be used, and where the request is coming from. The first placeholder is the name of the app requesting the operation. The second placeholder is the name of the secret.",
|
"comment" : "When the user performs a signature action using a secret, they are shown a prompt to approve the action. This is the description, showing which secret will be used, and where the request is coming from. The first placeholder is the name of the app requesting the operation. The second placeholder is the name of the secret.",
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -5522,7 +5504,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "assinar um pedido de “%1$(appName)@” usando o segredo “%2$(secretName)@”"
|
"value" : "assinar requisição a partir do “%1$(appName)@” utilizando o segredo “%2$(secretName)@”"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -5953,17 +5935,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth_context_signing_for_namespace" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Signing for %1$(namespace)@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"certificate_detail_critical_options_label" : {
|
"certificate_detail_critical_options_label" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -5972,12 +5943,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Critical Options"
|
"value" : "Critical Options"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Opções críticas"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5989,12 +5954,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Extensions"
|
"value" : "Extensions"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Extensões"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6006,12 +5965,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Key ID"
|
"value" : "Key ID"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "ID da Chave"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6023,12 +5976,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Certificate Path"
|
"value" : "Certificate Path"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Caminho do Certificado"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6040,12 +5987,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Principals"
|
"value" : "Principals"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Entidades Principais"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6057,12 +5998,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Serial Number"
|
"value" : "Serial Number"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Número de Série"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6074,12 +6009,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Public Key Fingerprint"
|
"value" : "Public Key Fingerprint"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Impressão Digital da Chave Pública"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6091,12 +6020,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Signing CA Fingerprint"
|
"value" : "Signing CA Fingerprint"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Impressão Digital da AC de Assinatura"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6108,12 +6031,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Valid After"
|
"value" : "Valid After"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Válido após"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6125,12 +6042,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Valid Until"
|
"value" : "Valid Until"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Válido até"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6142,28 +6053,11 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Validity Range"
|
"value" : "Validity Range"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Intervalo de validade"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Certificates" : {
|
"Certificates" : {
|
||||||
|
|
||||||
},
|
|
||||||
"certificates_section_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Certificates"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"copyable_click_to_copy_button" : {
|
"copyable_click_to_copy_button" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -6291,7 +6185,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Clique para copiar"
|
"value" : "Clique para Copiar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -6846,7 +6740,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Se você mudar suas configurações de biometria de _qualquer forma_, incluindo adicionar uma nova impressão digital, esta chave não será mais acessível."
|
"value" : "Se você mudar suas configurações de biometria de _qualquer forma_, incluindo adicionar uma nova impressão digital, esta chave não estará mais acessível."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -7401,7 +7295,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Isso é exibido ao final da sua chave pública. É geralmente um endereço de email."
|
"value" : "Isto é exibido ao final da sua chave pública. É geralmente um endereço de email."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -7955,8 +7849,8 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "new",
|
||||||
"value" : "Indisponível nessa versão do macOS"
|
"value" : "Unavailable on this version of macOS"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -8141,7 +8035,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Aviso: Chaves ML-DSA são muito novas e ainda não suportadas por vários servidores. Por favor, verifique se o servidor em que você usará essa chave aceita chaves ML-DSA."
|
"value" : "Aviso: Chaves ML-DSA são muito novas e não são suportadas por muitos servidores ainda. Por favor, verifique se o servidor que você estará utilizando esta chave aceita chaves ML-DSA."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -8696,7 +8590,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Nenhuma autenticação é solicitada enquanto seu Mac estiver desbloqueado, mas você será notificado quando um segredo for usado."
|
"value" : "Autenticação não é requerida enquanto seu Mac estiver destravado, mas você será notificado quando um segredo for utilizado."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -9621,7 +9515,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Você será solicitado a autenticar usando Touch ID, Apple Watch ou senha a cada uso."
|
"value" : "Você será requerido a autenticar utilizando Touch ID, Apple Watch ou senha antes de cada uso."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -9806,7 +9700,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Requer autenticação"
|
"value" : "Requer Autenticação"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -9991,7 +9885,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Criar um novo Segredo"
|
"value" : "Criar um Novo Segredo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -10176,7 +10070,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Não apagar"
|
"value" : "Não Apagar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -10546,7 +10440,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Se você apagar %1$(secretName)@, não poderá recuperá-lo. Digite “%2$(confirmSecretName)@” para confirmar."
|
"value" : "Se você deletar %1$(secretName)@, você não será permitido recuperá-lo. Digite “%2$(confirmSecretName)@” para confirmar."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -10731,7 +10625,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Apagar %1$(name)@?"
|
"value" : "Deletar %1$(name)@?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -11101,7 +10995,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Salvar"
|
"value" : "Renomear"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -11286,7 +11180,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Crie um novo clicando aqui."
|
"value" : "Criar um novo clicando aqui."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -11656,7 +11550,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Parece que você pode ter atualizado o macOS recentemente. Às vezes, isso coloca o Secure Enclave em um estado estranho, e talvez você precise reiniciar seu Mac antes que as coisas comecem a funcionar novamente."
|
"value" : "Parece que você pode ter atualizado recentemente o macOS. Às vezes, isto coloca o Secure Enclave em um estado estranho, e talvez você precise reiniciar seu Mac antes que as coisas comecem a funcionar novamente."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -11841,7 +11735,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Segredos faltando?"
|
"value" : "Credenciais faltando?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -12026,7 +11920,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Use a ferramenta de gestão do seu Smart Card para criar um segredo."
|
"value" : "Utilize sua ferramenta de gestão de Smart Card para criar um segredo."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -12211,7 +12105,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O Secretive suporta chaves EC256, EC384 e RSA2048."
|
"value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -12454,6 +12348,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"example.com" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"export SSH_AUTH_SOCK=%@" : {
|
"export SSH_AUTH_SOCK=%@" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -13309,7 +13206,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Há uma lista de instruções para aplicativos no GitHub, mantida pela comunidade. Se o aplicativo que você está procurando não é suportado, registre uma issue e a comunidade deve conseguir ajudar."
|
"value" : "Há uma lista de instruções para aplicativos no GitHub, mantida pela comunidade. Se o aplicativo que você está procurando não é suportado, registre um ticket para que a comunidade possa ajudar."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -13494,7 +13391,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Há uma lista de instruções para shells no GitHub, mantida pela comunidade. Se o shell que você está procurando não é suportado, registre uma issue e a comunidade deve conseguir ajudar."
|
"value" : "Há uma lista de instruções para aplicativos no GitHub, mantida pela comunidade. Se o aplicativo que você está procurando não é suportado, registre um ticket para que a comunidade possa ajudar."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -13562,12 +13459,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "your_email@example.com"
|
"value" : "your_email@example.com"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "seu_email@exemplo.com.br"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -13579,12 +13470,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "The email address you set when you configured git (visible in gitconfig)."
|
"value" : "The email address you set when you configured git (visible in gitconfig)."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "O endereço de email que você definiu quando configurou o git (visível no gitconfig)."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -13596,12 +13481,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Email Address"
|
"value" : "Email Address"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Endereço de email"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -13731,7 +13610,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Você precisará criar um Segredo antes de configurar essa ação."
|
"value" : "Você precisará criar uma Credencial antes de configurar esta ação."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -13916,7 +13795,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Configurar usando Segredo"
|
"value" : "Configurar Usando Credencial"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -14101,7 +13980,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Sem Segredo"
|
"value" : "Sem Credenciais"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -14286,7 +14165,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Segredo"
|
"value" : "Credencial"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -14471,7 +14350,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Você pode configurar mais de uma ferramenta, elas geralmente não interferem umas com as outras."
|
"value" : "Você pode configurar mais de uma ferramenta, eles geralmente não interferem uns com os outros."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -15026,7 +14905,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Se você estiver tentando assinar seus commits no git, configure Assinatura no Git."
|
"value" : "Se você estiver tentando registrar seus commits no git, configure o Git Signing."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -15211,7 +15090,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Se você está tentando configurar qualquer coisa em sua linha de comando para usar o Secretive, configure seu shell."
|
"value" : "Se você está tentando configurar qualquer coisa em sua linha de comando para usar com Secretive, configure seu shell."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -15396,7 +15275,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Se você não sabe qual shell usa e não o alterou, você provavelmente está usando `%(shellName)@`."
|
"value" : "Se você não sabia qual shell você usa e não alterou, você provavelmente está usando `%(shellName)@`."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -15766,7 +15645,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Configurando ferramentas para o Secretive"
|
"value" : "Configurando Ferramentas para Secretive"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -15951,7 +15830,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "A maioria das ferramentas tentará procurar chaves SSH no disco em `~/.ssh`. Para usar o Secretive, precisamos configurar essas ferramentas para falarem com o Secretive ao invés disso."
|
"value" : "A maioria das ferramentas tentará procurar chaves SSH no disco em `~/.ssh`. Para usar o Secretive, precisamos configurar essas ferramentas para comunicar com o Secretive."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -16136,7 +16015,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O que devo configurar?"
|
"value" : "O que devo Configurar?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -17247,7 +17126,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Arquivo de configuração"
|
"value" : "Arquivo de Configuração"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -17617,7 +17496,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Você pode dizer ao SSH para usar uma chave específica com determinado host. Consulte a documentação na web para mais detalhes."
|
"value" : "Você pode pedir ao SSH para usar uma chave específica para um determinado host. Consulte a documentação na web para mais detalhes."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -18359,7 +18238,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Assinatura no Git"
|
"value" : "Assinatura Git"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -18916,7 +18795,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Ver no GitHub"
|
"value" : "Visualizar no GitHub"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -19101,7 +18980,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Ver documentação na web"
|
"value" : "Ver Documentação na Web"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -19344,6 +19223,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Key Properties" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"no_secure_storage_description" : {
|
"no_secure_storage_description" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -19656,7 +19538,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Sem Armazenamento Seguro disponível"
|
"value" : "Sem Armazenamento Seguro Disponível"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -19841,7 +19723,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Se você procura adicionar um para seu Mac, o YubiKey 5 Series é muito bom."
|
"value" : "Se você está buscando adicionar um para seu Mac, o YubiKey 5 Series é muito bom."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -19900,39 +19782,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pending_request_description" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pending_requests_review_batch_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Review as Batch"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pending_requests_review_single_button" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Review"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"persist_authentication_accept_button" : {
|
"persist_authentication_accept_button" : {
|
||||||
"comment" : "When the user authorizes an action using a secret that requires unlock, they're shown a notification offering to leave the secret unlocked for a set period of time. This is the title for the notification.",
|
"comment" : "When the user authorizes an action using a secret that requires unlock, they're shown a notification offering to leave the secret unlocked for a set period of time. This is the title for the notification.",
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -20060,7 +19909,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Manter destrancado"
|
"value" : "Deixar Destrancado"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -20246,7 +20095,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Não destrancar"
|
"value" : "Não Destravar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -20313,12 +20162,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Name"
|
"value" : "Name"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Nome"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -20330,14 +20173,11 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Certificate Name"
|
"value" : "Certificate Name"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Nome do Certificado"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Restrictions" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"reveal_in_finder_button" : {
|
"reveal_in_finder_button" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -20532,12 +20372,6 @@
|
|||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Matching Certificates"
|
"value" : "Matching Certificates"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Certificados correspondentes"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -21592,7 +21426,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Editar"
|
"value" : "Renomear"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -22141,7 +21975,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Esse aplicativo auxiliar se chama **Secret Agent** e você pode vê-lo no Monitor de Atividades de tempos em tempos."
|
"value" : "Este aplicativo de ajuda é chamado **Secret Agent** e você pode vê-lo no Monitor de Atividades de tempo em tempo."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -22326,7 +22160,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O Secretive precisa configurar um aplicativo auxiliar para funcionar corretamente. Ele irá assinar pedidos de clientes SSH em segundo plano, para que você não precise manter o aplicativo principal do Secretive aberto."
|
"value" : "Secretive precisa configurar um aplicativo de ajuda para funcionar corretamente. Isso irá assinar requisições de clientes SSH no plano de fundo para que você não precise manter o aplicativo Secretive aberto."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -22696,7 +22530,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Configurar Agente"
|
"value" : "Configurar Agent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -23621,7 +23455,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "O Secretive vai verificar o GitHub periodicamente para saber se há uma nova versão. Se notar requisições de rede para o GitHub, esse é o motivo."
|
"value" : "Secretive irá periodicamente verificar com o GitHub para verificar se existe uma nova versão. Se você ver alguma requisição de rede para o GitHub, este é o porque."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -24548,7 +24382,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Usando o segredo %1$(secretName)@"
|
"value" : "Utilizando o segredo %1$(secretName)@"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -24734,7 +24568,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Assinado pedido de %1$(appName)@"
|
"value" : "Requisição Assinada fr %1$(appName)@"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -25105,7 +24939,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Smart Card"
|
"value" : "Cartões com chip"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -25163,6 +24997,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Specific" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"unnamed_secret" : {
|
"unnamed_secret" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -25290,7 +25127,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Sem nome"
|
"value" : "Sem Nome"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -25475,7 +25312,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Atualização Crítica de Segurança necessária"
|
"value" : "Atualização Crítica de Segurança Requerida"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -25845,7 +25682,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Atualização disponível"
|
"value" : "Atualização Disponível"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -26589,7 +26426,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Clique para atualizar"
|
"value" : "Clique para Atualizar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -26960,7 +26797,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Notas de Lançamento"
|
"value" : "Notas de Mudanças"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -27145,7 +26982,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Build de Teste"
|
"value" : "Versão de Teste"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
@@ -27700,7 +27537,7 @@
|
|||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Baixar a Nightly Build mais recente"
|
"value" : "Baixar a última Nightly Build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ import XPCWrappers
|
|||||||
self.currentVersion = currentVersion
|
self.currentVersion = currentVersion
|
||||||
_ = Task {
|
_ = Task {
|
||||||
if checkOnLaunch {
|
if checkOnLaunch {
|
||||||
try await checkForUpdates()
|
try? await checkForUpdates()
|
||||||
}
|
}
|
||||||
while true {
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import Security
|
import Security
|
||||||
import OSLog
|
import os
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
@Observable @MainActor public final class CertificateStore: Sendable {
|
@Observable @MainActor public final class CertificateStore: Sendable {
|
||||||
@@ -67,7 +67,6 @@ import SecretKit
|
|||||||
public func update(certificate: Certificate) throws {
|
public func update(certificate: Certificate) throws {
|
||||||
let updateQuery = KeychainDictionary([
|
let updateQuery = KeychainDictionary([
|
||||||
kSecClass: Constants.keyClass,
|
kSecClass: Constants.keyClass,
|
||||||
kSecAttrService: Constants.keyTag,
|
|
||||||
kSecAttrAccount: certificate.id,
|
kSecAttrAccount: certificate.id,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -135,7 +134,7 @@ extension CertificateStore {
|
|||||||
|
|
||||||
enum Constants {
|
enum Constants {
|
||||||
static let keyClass = kSecClassGenericPassword as String
|
static let keyClass = kSecClassGenericPassword as String
|
||||||
static let keyTag = "com.maxgoedjen.certificatestore.opensshcertificate"
|
static let keyTag = Data("com.maxgoedjen.certificatestore.opensshcertificate".utf8)
|
||||||
static let notificationToken = UUID().uuidString
|
static let notificationToken = UUID().uuidString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,262 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import UniformTypeIdentifiers
|
|
||||||
|
|
||||||
public struct MultilineInfoView<TitleView: View, ItemView: View>: View {
|
|
||||||
|
|
||||||
var titleView: TitleView
|
|
||||||
var items: ItemView
|
|
||||||
|
|
||||||
public init(@ViewBuilder titleView: () -> TitleView, @ContentBuilder items: () -> ItemView) {
|
|
||||||
self.titleView = titleView()
|
|
||||||
self.items = items()
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(title: LocalizedStringResource, subtitle: LocalizedStringResource? = nil, image: Image, items: [String]) where TitleView == FixedTitleView, ItemView == FixedItemsView {
|
|
||||||
self.init {
|
|
||||||
FixedTitleView(title: title, subtitle: subtitle, image: image)
|
|
||||||
} items: {
|
|
||||||
FixedItemsView(items: items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public init(title: LocalizedStringResource, subtitle: LocalizedStringResource? = nil, image: Image, @ContentBuilder items: () -> ItemView) where TitleView == FixedTitleView {
|
|
||||||
self.init {
|
|
||||||
FixedTitleView(title: title, subtitle: subtitle, image: image)
|
|
||||||
} items: {
|
|
||||||
items()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
titleView
|
|
||||||
.bold()
|
|
||||||
.safeAreaPadding(20)
|
|
||||||
Group(subviews: items) { subviews in
|
|
||||||
ForEach(subviews: subviews) { subview in
|
|
||||||
Divider()
|
|
||||||
.ignoresSafeArea()
|
|
||||||
.opacity(subview.id == subviews.first?.id ? 1 : 0.5)
|
|
||||||
HoveringSubview {
|
|
||||||
subview
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
._background(interactionState: .normal)
|
|
||||||
.frame(minWidth: 150, maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HoveringSubview<Content: View>: View {
|
|
||||||
|
|
||||||
@State private var multilineAction: MultilineItemActionKey.Box?
|
|
||||||
private let content: Content
|
|
||||||
@State private var interactionState: InteractionState = .normal
|
|
||||||
|
|
||||||
init(@ViewBuilder _ content: () -> Content) {
|
|
||||||
self.multilineAction = nil
|
|
||||||
self.content = content()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
content
|
|
||||||
Spacer()
|
|
||||||
if let image = multilineAction?.image {
|
|
||||||
image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.safeAreaPadding(20)
|
|
||||||
.onHover { hovering in
|
|
||||||
withAnimation {
|
|
||||||
guard multilineAction != nil else { return }
|
|
||||||
interactionState = hovering ? .hovering : .normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.gesture(
|
|
||||||
TapGesture()
|
|
||||||
.onEnded {
|
|
||||||
multilineAction?.closure()
|
|
||||||
withAnimation {
|
|
||||||
interactionState = .normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.backgroundStyle(.primary.opacity(interactionState == .hovering ? 0.3 : 0))
|
|
||||||
.onPreferenceChange(MultilineItemActionKey.self) {
|
|
||||||
multilineAction = $0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public struct FixedTitleView: View {
|
|
||||||
let title: LocalizedStringResource
|
|
||||||
let subtitle: LocalizedStringResource?
|
|
||||||
let image: Image
|
|
||||||
|
|
||||||
public init(title: LocalizedStringResource, subtitle: LocalizedStringResource?, image: Image) {
|
|
||||||
self.title = title
|
|
||||||
self.subtitle = subtitle
|
|
||||||
self.image = image
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
HStack {
|
|
||||||
image
|
|
||||||
.renderingMode(.template)
|
|
||||||
.imageScale(.large)
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(title)
|
|
||||||
.font(.headline)
|
|
||||||
if let subtitle {
|
|
||||||
Text(subtitle)
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct FixedItemsView: View {
|
|
||||||
|
|
||||||
let items: [String]
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
ForEach(Array(items.enumerated()), id: \.offset) {
|
|
||||||
Text($0.element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
.contentShape(RoundedRectangle(cornerRadius: cornerRadius))
|
|
||||||
.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: Color
|
|
||||||
if #available(macOS 27.0, *) {
|
|
||||||
base = .clear
|
|
||||||
} else {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct MultilineItemAction: ViewModifier {
|
|
||||||
|
|
||||||
let image: Image?
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
public init(image: Image?, action: @escaping () -> Void) {
|
|
||||||
self.image = image
|
|
||||||
self.action = action
|
|
||||||
}
|
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
|
||||||
content
|
|
||||||
.preference(key: MultilineItemActionKey.self, value: MultilineItemActionKey.Box(image: image, closure: action))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
public func multilineItemAction(image: Image? = nil, action: @escaping () -> Void) -> some View {
|
|
||||||
modifier(MultilineItemAction(image: image, action: action))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct MultilineItemActionKey : @MainActor PreferenceKey, ~Sendable {
|
|
||||||
|
|
||||||
public struct Box: Equatable {
|
|
||||||
|
|
||||||
let id: UUID = UUID()
|
|
||||||
let image: Image?
|
|
||||||
let closure: () -> Void
|
|
||||||
|
|
||||||
public static func == (lhs: borrowing MultilineItemActionKey.Box, rhs: borrowing MultilineItemActionKey.Box) -> Bool {
|
|
||||||
lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public typealias Value = Box?
|
|
||||||
@MainActor public static let defaultValue: Box? = nil
|
|
||||||
|
|
||||||
public static func reduce(value: inout Box?, nextValue: () -> Box?) {
|
|
||||||
value = nextValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
MultilineInfoView {
|
|
||||||
Text("Hello")
|
|
||||||
} items: {
|
|
||||||
Text("World")
|
|
||||||
.multilineItemAction(image: Image(systemName: "person.wave")) {
|
|
||||||
print("Hello")
|
|
||||||
}
|
|
||||||
Text("World")
|
|
||||||
Text("World")
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
#Preview {
|
|
||||||
MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world.", "Hello world."])
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
@@ -142,64 +142,62 @@ extension SSHAgentInputParser {
|
|||||||
case SSHAgent.ProtocolExtension.OpenSSHExtension.domain:
|
case SSHAgent.ProtocolExtension.OpenSSHExtension.domain:
|
||||||
switch name {
|
switch name {
|
||||||
case SSHAgent.ProtocolExtension.OpenSSHExtension.sessionBind(.empty).name:
|
case SSHAgent.ProtocolExtension.OpenSSHExtension.sessionBind(.empty).name:
|
||||||
do {
|
let hostkeyBlob = try reader.readNextChunkAsSubReader()
|
||||||
let hostkeyBlob = try reader.readNextChunkAsSubReader()
|
let hostKeyType = try hostkeyBlob.readNextChunkAsString()
|
||||||
let hostKeyType = try hostkeyBlob.readNextChunkAsString()
|
let hostKeyData = try hostkeyBlob.readNextChunk()
|
||||||
let hostKeyData = try hostkeyBlob.readNextChunk()
|
let sessionID = try reader.readNextChunk()
|
||||||
let sessionID = try reader.readNextChunk()
|
let signatureBlob = try reader.readNextChunkAsSubReader()
|
||||||
let signatureBlob = try reader.readNextChunkAsSubReader()
|
_ = try signatureBlob.readNextChunk() // key type again
|
||||||
_ = try signatureBlob.readNextChunk() // key type again
|
let signature = try signatureBlob.readNextChunk()
|
||||||
let signature = try signatureBlob.readNextChunk()
|
let forwarding = try reader.readNextByteAsBool()
|
||||||
let forwarding = try reader.readNextByteAsBool()
|
switch hostKeyType {
|
||||||
switch hostKeyType {
|
// FIXME: FACTOR OUT?
|
||||||
case "ssh-ed25519":
|
case "ssh-ed25519":
|
||||||
let hostKey = try CryptoKit.Curve25519.Signing.PublicKey(rawRepresentation: hostKeyData)
|
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 {
|
guard hostKey.isValidSignature(signature, for: sessionID) else {
|
||||||
throw AgentParsingError.incorrectSignature
|
throw AgentParsingError.incorrectSignature
|
||||||
}
|
}
|
||||||
case "ecdsa-sha2-nistp256":
|
} else {
|
||||||
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":
|
|
||||||
throw AgentParsingError.unhandledRequest
|
|
||||||
default:
|
|
||||||
throw AgentParsingError.unhandledRequest
|
throw AgentParsingError.unhandledRequest
|
||||||
}
|
}
|
||||||
let context = SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext(
|
case "ssh-mldsa-87":
|
||||||
hostKey: hostKeyData,
|
if #available(macOS 26.0, *) {
|
||||||
sessionID: sessionID,
|
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
|
||||||
signature: signature,
|
guard hostKey.isValidSignature(signature, for: sessionID) else {
|
||||||
forwarding: forwarding
|
throw AgentParsingError.incorrectSignature
|
||||||
)
|
}
|
||||||
return .openSSH(.sessionBind(context))
|
} else {
|
||||||
} catch {
|
throw AgentParsingError.unhandledRequest
|
||||||
return .openSSH(.sessionBind(.empty))
|
}
|
||||||
|
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:
|
default:
|
||||||
return .openSSH(.unknown(String(name)))
|
return .openSSH(.unknown(String(name)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import SSHProtocolKit
|
|||||||
public final class Agent: Sendable {
|
public final class Agent: Sendable {
|
||||||
|
|
||||||
private let storeList: SecretStoreList
|
private let storeList: SecretStoreList
|
||||||
private let authenticationHandler: AuthenticationHandler
|
|
||||||
private let certificateStore: CertificateStore
|
private let certificateStore: CertificateStore
|
||||||
private let witness: SigningWitness?
|
private let witness: SigningWitness?
|
||||||
private let publicKeyWriter = OpenSSHPublicKeyWriter()
|
private let publicKeyWriter = OpenSSHPublicKeyWriter()
|
||||||
@@ -23,16 +22,10 @@ public final class Agent: Sendable {
|
|||||||
/// - 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(
|
public init(storeList: SecretStoreList, certificateStore: CertificateStore, witness: SigningWitness? = nil) {
|
||||||
storeList: SecretStoreList,
|
|
||||||
certificateStore: CertificateStore,
|
|
||||||
authenticationHandler: AuthenticationHandler,
|
|
||||||
witness: SigningWitness? = nil
|
|
||||||
) {
|
|
||||||
logger.debug("Agent is running")
|
logger.debug("Agent is running")
|
||||||
self.storeList = storeList
|
self.storeList = storeList
|
||||||
self.certificateStore = certificateStore
|
self.certificateStore = certificateStore
|
||||||
self.authenticationHandler = authenticationHandler
|
|
||||||
self.witness = witness
|
self.witness = witness
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,23 +79,19 @@ extension Agent {
|
|||||||
default:
|
default:
|
||||||
target = nil
|
target = nil
|
||||||
}
|
}
|
||||||
_ = target
|
|
||||||
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, target: target))
|
response.append(try await sign(data: context.dataToSign.raw, keyBlob: context.keyBlob, provenance: provenance, target: target))
|
||||||
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
|
||||||
case .protocolExtension(.openSSH(.sessionBind(let bind))):
|
case .protocolExtension(.openSSH(.sessionBind(let bind))):
|
||||||
// This is disabled until forward enforcement is handled.
|
|
||||||
_ = bind
|
|
||||||
response = try await MainActor.run {
|
response = try await MainActor.run {
|
||||||
logger.debug("Agent received bind request but not currently supported.")
|
guard sessionID == nil else {
|
||||||
throw UnhandledRequestError()
|
logger.error("Agent received bind request, but already bound.")
|
||||||
// guard sessionID == nil else {
|
// FIXME: This will break forwarding for now.
|
||||||
// logger.error("Agent received bind request, but already bound.")
|
throw BindingFailure()
|
||||||
// throw BindingFailure()
|
}
|
||||||
// }
|
logger.debug("Agent bound")
|
||||||
// logger.debug("Agent bound")
|
sessionID = bind
|
||||||
// sessionID = bind
|
return SSHAgent.Response.agentSuccess.data
|
||||||
// return SSHAgent.Response.agentSuccess.data
|
|
||||||
}
|
}
|
||||||
logger.debug("Agent returned \(SSHAgent.Response.agentSuccess.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.Response.agentSuccess.debugDescription)")
|
||||||
case .unknown(let value):
|
case .unknown(let value):
|
||||||
@@ -158,27 +147,39 @@ extension Agent {
|
|||||||
logger.debug("Agent did not have a key matching \(keyBlobHex)")
|
logger.debug("Agent did not have a key matching \(keyBlobHex)")
|
||||||
throw NoMatchingKeyError()
|
throw NoMatchingKeyError()
|
||||||
}
|
}
|
||||||
logger.debug("Agent offering witness chance to object")
|
|
||||||
do {
|
|
||||||
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance, target: target)
|
|
||||||
} catch {
|
|
||||||
logger.debug("Witness objected")
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
logger.debug("Witness did not object")
|
|
||||||
|
|
||||||
let request = SignatureRequest(secret: secret, provenance: provenance, target: target)
|
try evaluateRestrictions(secret.attributes.restrictions, from: provenance, for: target)
|
||||||
let newContext = AuthenticationContext(secret: secret, requests: [request])
|
|
||||||
let context = try await authenticationHandler.authenticatedContext(for: request, context: newContext)
|
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance, target: target)
|
||||||
let result = try await store.sign(data: data, with: secret, for: provenance, target: target, context: context?.laContext)
|
|
||||||
let signedData = signatureWriter.data(secret: secret, signature: result)
|
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance, target: target)
|
||||||
try await witness?.witness(accessTo: secret, from: store, by: provenance, target: target, offerPersistence: secret.authenticationRequirement.required)
|
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
||||||
|
|
||||||
|
try await witness?.witness(accessTo: secret, from: store, by: provenance, target: target)
|
||||||
|
|
||||||
logger.debug("Agent signed request")
|
logger.debug("Agent signed request")
|
||||||
|
|
||||||
return signedData
|
return signedData
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Agent {
|
||||||
|
|
||||||
|
func evaluateRestrictions(_ restrictions: Restrictions?, from provenance: SigningRequestProvenance, for target: SigningRequestTarget?) throws(RestrictionError) {
|
||||||
|
guard let restrictions else { return }
|
||||||
|
print(restrictions)
|
||||||
|
throw .connectionsNotPermitted
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RestrictionError: Error {
|
||||||
|
case signingNotPermitted
|
||||||
|
case connectionsNotPermitted
|
||||||
|
case hostNotAllowed(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
/// Gives any store with no loaded secrets a chance to reload.
|
/// Gives any store with no loaded secrets a chance to reload.
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
@unsafe @preconcurrency import LocalAuthentication
|
|
||||||
import SecretKit
|
|
||||||
import OSLog
|
|
||||||
|
|
||||||
/// A context describing a persisted authentication.
|
|
||||||
public final class AuthenticationContext: AuthenticationContextProtocol {
|
|
||||||
|
|
||||||
/// The Secret to persist authentication for.
|
|
||||||
public let secret: AnySecret
|
|
||||||
/// The LAContext used to authorize the persistent context.
|
|
||||||
public let laContext: LAContext?
|
|
||||||
|
|
||||||
enum Validity {
|
|
||||||
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
|
|
||||||
case time(monotonicExpiration: UInt64)
|
|
||||||
case requestIDs(Set<UUID>)
|
|
||||||
case exclusive(UUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
let validity: Validity
|
|
||||||
|
|
||||||
/// Initializes a context.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - secret: The Secret to persist authentication for.
|
|
||||||
/// - duration: The duration of the authorization context, in seconds.
|
|
||||||
init<SecretType: Secret>(secret: SecretType, duration: TimeInterval) {
|
|
||||||
self.secret = AnySecret(secret)
|
|
||||||
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
|
|
||||||
self.validity = .time(monotonicExpiration: clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds))
|
|
||||||
let newContext = LAContext()
|
|
||||||
newContext.touchIDAuthenticationAllowableReuseDuration = duration
|
|
||||||
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
|
||||||
|
|
||||||
let formatter = DateComponentsFormatter()
|
|
||||||
formatter.unitsStyle = .spellOut
|
|
||||||
formatter.allowedUnits = [.hour, .minute, .day]
|
|
||||||
let durationString = formatter.string(from: duration)!
|
|
||||||
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
|
|
||||||
laContext = newContext
|
|
||||||
}
|
|
||||||
|
|
||||||
init<SecretType: Secret>(secret: SecretType, requests: Set<SignatureRequest>) {
|
|
||||||
self.secret = AnySecret(secret)
|
|
||||||
if requests.count == 1 {
|
|
||||||
self.validity = .exclusive(requests.first!.id)
|
|
||||||
} else {
|
|
||||||
self.validity = .requestIDs(Set(requests.map(\.id)))
|
|
||||||
}
|
|
||||||
if secret.authenticationRequirement.required {
|
|
||||||
let newContext = LAContext()
|
|
||||||
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
|
||||||
let appName = requests.first!.provenance.origin.displayName
|
|
||||||
if requests.count > 1 {
|
|
||||||
newContext.localizedReason = String(localized: .authContextRequestMultiple(appName: appName, secretName: secret.name))
|
|
||||||
} else {
|
|
||||||
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: appName, secretName: secret.name))
|
|
||||||
}
|
|
||||||
laContext = newContext
|
|
||||||
} else {
|
|
||||||
laContext = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A boolean describing whether or not the context is still valid.
|
|
||||||
public func valid(for request: SignatureRequest) -> Bool {
|
|
||||||
switch validity {
|
|
||||||
case .time(let monotonicExpiration):
|
|
||||||
clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration
|
|
||||||
case .requestIDs(let set):
|
|
||||||
set.contains(request.id)
|
|
||||||
case .exclusive(let id):
|
|
||||||
id == request.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func evaluate(requireBiometrics: Bool) async throws -> Bool {
|
|
||||||
guard let laContext else { return false }
|
|
||||||
let policy: LAPolicy = requireBiometrics ? .deviceOwnerAuthenticationWithBiometrics : .deviceOwnerAuthentication
|
|
||||||
return try await laContext.evaluatePolicy(policy, localizedReason: laContext.localizedReason)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func cancel() async {
|
|
||||||
laContext?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor public protocol AuthenticationHandlerProtocol: Observable {
|
|
||||||
var batchableRequests: [[SignatureRequest]] { get }
|
|
||||||
func setPendingRequestHandler(_ handler: @escaping () async throws -> Void)
|
|
||||||
func authenticatedContext(for request: SignatureRequest, context: any AuthenticationContextProtocol) async throws -> (any AuthenticationContextProtocol)?
|
|
||||||
func persistAuthentication<SecretType: Secret>(secret: SecretType, forDuration duration: TimeInterval) async throws
|
|
||||||
func requestAuthentication(for requests: Set<SignatureRequest>) async throws
|
|
||||||
}
|
|
||||||
|
|
||||||
@Observable @MainActor public class AuthenticationHandler: AuthenticationHandlerProtocol {
|
|
||||||
|
|
||||||
private var authenticatedContexts: [AnySecret: AuthenticationContext] = [:]
|
|
||||||
private var waitingRequests: Set<SignatureRequest> = []
|
|
||||||
private var activeTask: Task<Bool, any Error>?
|
|
||||||
private var activeContext: (any AuthenticationContextProtocol)?
|
|
||||||
|
|
||||||
private var lastBatchAuthPresentation: Set<SignatureRequest>?
|
|
||||||
private var presentPendingAuth: (() async throws -> Void)?
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AuthenticationHandler")
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setPendingRequestHandler(_ handler: @escaping () async throws -> Void) {
|
|
||||||
self.presentPendingAuth = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
public func authenticatedContext(for request: SignatureRequest, context: any AuthenticationContextProtocol) async throws -> (any AuthenticationContextProtocol)? {
|
|
||||||
if request.secret.authenticationRequirement.required {
|
|
||||||
// Slow path, will block caller until authenticated (either directly or via a pending requests view).
|
|
||||||
return try await waitForAuthentication(for: request, context: context)
|
|
||||||
} else {
|
|
||||||
// Fast path, no blocking/enqueing required
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForAuthentication(for request: SignatureRequest, context: any AuthenticationContextProtocol) async throws -> any AuthenticationContextProtocol {
|
|
||||||
logger.log("Entering waitForAuthentication for \(request.id)")
|
|
||||||
if let existing = existingAuthenticationContext(for: request) {
|
|
||||||
logger.log("Short circuiting wait, existing valid context already exists.")
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
waitingRequests.insert(request)
|
|
||||||
logger.log("Waiting for authentication for \(request.id)")
|
|
||||||
defer {
|
|
||||||
logger.log("Removed hold for \(request.id)")
|
|
||||||
waitingRequests.remove(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
if waitingRequests.count > 1 {
|
|
||||||
return try await waitUntilRequestActedOn(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hold onto the task and context so we can cancel them when prsenting pending.
|
|
||||||
activeContext = context
|
|
||||||
let currentTask = Task<Bool, any Error> {
|
|
||||||
logger.log("Beginning individual auth prompt")
|
|
||||||
let result = (try? await context.evaluate(requireBiometrics: request.secret.authenticationRequirement == .biometryCurrent)) ?? false
|
|
||||||
logger.log("Ended individual auth prompt")
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
activeTask = currentTask
|
|
||||||
let result = try? await activeTask?.value
|
|
||||||
if result == false && activeTask?.isCancelled == false {
|
|
||||||
waitingRequests.remove(request)
|
|
||||||
throw CancellationError()
|
|
||||||
} else if currentTask.isCancelled {
|
|
||||||
return try await waitUntilRequestActedOn(request)
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitUntilRequestActedOn(_ request: SignatureRequest) async throws -> any AuthenticationContextProtocol {
|
|
||||||
logger.log("Auth prompt was cancelled, waiting for explicit auth")
|
|
||||||
// At this point, we essentially just block the task until either the request has been authenticated "externally" via the pending view.
|
|
||||||
while waitingRequests.contains(request) {
|
|
||||||
if waitingRequests != lastBatchAuthPresentation {
|
|
||||||
// If we're about to present a batch, we cancel the individual auth prompt, and show the batch one.
|
|
||||||
logger.log("Multiple pending requests exist, cancelling existing auth prompt")
|
|
||||||
activeTask?.cancel()
|
|
||||||
lastBatchAuthPresentation = waitingRequests
|
|
||||||
logger.log("Requesting pending requests presentation")
|
|
||||||
try await presentPendingAuth?()
|
|
||||||
await activeContext?.cancel()
|
|
||||||
logger.log("Requested pending requests presentation")
|
|
||||||
}
|
|
||||||
if let preauthenticated = existingAuthenticationContext(for: request) {
|
|
||||||
logger.log("Explicit auth context found")
|
|
||||||
return preauthenticated
|
|
||||||
}
|
|
||||||
try await Task.sleep(for: .milliseconds(100))
|
|
||||||
}
|
|
||||||
throw CancellationError()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public var batchableRequests: [[SignatureRequest]] {
|
|
||||||
waitingRequests.reduce(into: [:]) { partialResult, next in
|
|
||||||
partialResult[next.batchID, default: []].append(next)
|
|
||||||
}
|
|
||||||
.values
|
|
||||||
.map { $0.sorted() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func existingAuthenticationContext(for request: SignatureRequest) -> (any AuthenticationContextProtocol)? {
|
|
||||||
guard let authenticated = authenticatedContexts[request.secret], authenticated.valid(for: request) else { return nil }
|
|
||||||
return authenticated
|
|
||||||
}
|
|
||||||
|
|
||||||
public func persistAuthentication<SecretType: Secret>(secret: SecretType, forDuration duration: TimeInterval) async throws {
|
|
||||||
let context = AuthenticationContext(secret: secret, duration: duration)
|
|
||||||
let success = try await context.evaluate(requireBiometrics: secret.authenticationRequirement == .biometryCurrent)
|
|
||||||
guard success else { return }
|
|
||||||
authenticatedContexts[AnySecret(secret)] = context
|
|
||||||
}
|
|
||||||
|
|
||||||
public func requestAuthentication(for requests: Set<SignatureRequest>) async throws {
|
|
||||||
activeTask?.cancel()
|
|
||||||
guard let first = requests.first else { return }
|
|
||||||
let context = AuthenticationContext(secret: first.secret, requests: requests)
|
|
||||||
let success = (try? await context.evaluate(requireBiometrics: first.secret.authenticationRequirement == .biometryCurrent)) ?? false
|
|
||||||
guard success else {
|
|
||||||
waitingRequests.subtract(requests)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Even single-use ones get stuffed into authenticatedContexts, so that it can unblock the response path.
|
|
||||||
authenticatedContexts[AnySecret(first.secret)] = context
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -44,7 +44,6 @@ extension SigningRequestTracer {
|
|||||||
|
|
||||||
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
|
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
|
||||||
_ = unsafe proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
|
_ = unsafe proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
|
||||||
|
|
||||||
let path = unsafe String(cString: pathPointer)
|
let path = unsafe String(cString: pathPointer)
|
||||||
var secCode: Unmanaged<SecCode>!
|
var secCode: Unmanaged<SecCode>!
|
||||||
let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks]
|
let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks]
|
||||||
@@ -81,6 +80,10 @@ extension SigningRequestTracer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// from libproc.h
|
||||||
|
@_silgen_name("proc_pidpath")
|
||||||
|
@discardableResult func proc_pidpath(_ pid: Int32, _ buffer: UnsafeMutableRawPointer!, _ buffersize: UInt32) -> Int32
|
||||||
|
|
||||||
//// from SecTask.h
|
//// from SecTask.h
|
||||||
@_silgen_name("SecCodeCreateWithPID")
|
@_silgen_name("SecCodeCreateWithPID")
|
||||||
@discardableResult func SecCodeCreateWithPID(_: Int32, _: SecCSFlags, _: UnsafeMutablePointer<Unmanaged<SecCode>?>!) -> OSStatus
|
@discardableResult func SecCodeCreateWithPID(_: Int32, _: SecCSFlags, _: UnsafeMutablePointer<Unmanaged<SecCode>?>!) -> OSStatus
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ public protocol SigningWitness: Sendable {
|
|||||||
/// - secret: The `Secret` that will was used to sign the request.
|
/// - secret: The `Secret` that will was used to sign the request.
|
||||||
/// - store: The `Store` that signed the request..
|
/// - store: The `Store` that signed the request..
|
||||||
/// - provenance: A `SigningRequestProvenance` object describing the origin of the request.
|
/// - provenance: A `SigningRequestProvenance` object describing the origin of the request.
|
||||||
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?, offerPersistence: Bool) async throws
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public struct SocketController {
|
|||||||
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.
|
||||||
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted, object: fileHandle)
|
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted)
|
||||||
|
|
||||||
fileHandle.acceptConnectionInBackgroundAndNotify()
|
fileHandle.acceptConnectionInBackgroundAndNotify()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import LocalAuthentication
|
||||||
|
|
||||||
|
/// A context describing a persisted authentication.
|
||||||
|
package final class PersistentAuthenticationContext<SecretType: Secret>: PersistedAuthenticationContext {
|
||||||
|
|
||||||
|
/// The Secret to persist authentication for.
|
||||||
|
let secret: SecretType
|
||||||
|
/// The LAContext used to authorize the persistent context.
|
||||||
|
package nonisolated(unsafe) let context: LAContext
|
||||||
|
/// An expiration date for the context.
|
||||||
|
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
|
||||||
|
let monotonicExpiration: UInt64
|
||||||
|
|
||||||
|
/// Initializes a context.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - secret: The Secret to persist authentication for.
|
||||||
|
/// - context: The LAContext used to authorize the persistent context.
|
||||||
|
/// - duration: The duration of the authorization context, in seconds.
|
||||||
|
init(secret: SecretType, context: LAContext, duration: TimeInterval) {
|
||||||
|
self.secret = secret
|
||||||
|
unsafe self.context = context
|
||||||
|
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
|
||||||
|
self.monotonicExpiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A boolean describing whether or not the context is still valid.
|
||||||
|
package var valid: Bool {
|
||||||
|
clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration
|
||||||
|
}
|
||||||
|
|
||||||
|
package var expiration: Date {
|
||||||
|
let remainingNanoseconds = monotonicExpiration - clock_gettime_nsec_np(CLOCK_MONOTONIC)
|
||||||
|
let remainingInSeconds = Measurement(value: Double(remainingNanoseconds), unit: UnitDuration.nanoseconds).converted(to: .seconds).value
|
||||||
|
return Date(timeIntervalSinceNow: remainingInSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package actor PersistentAuthenticationHandler<SecretType: Secret>: Sendable {
|
||||||
|
|
||||||
|
private var persistedAuthenticationContexts: [SecretType: PersistentAuthenticationContext<SecretType>] = [:]
|
||||||
|
|
||||||
|
package init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
package func existingPersistedAuthenticationContext(secret: SecretType) -> PersistentAuthenticationContext<SecretType>? {
|
||||||
|
guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil }
|
||||||
|
return persisted
|
||||||
|
}
|
||||||
|
|
||||||
|
package func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws {
|
||||||
|
let newContext = LAContext()
|
||||||
|
newContext.touchIDAuthenticationAllowableReuseDuration = duration
|
||||||
|
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
||||||
|
|
||||||
|
let formatter = DateComponentsFormatter()
|
||||||
|
formatter.unitsStyle = .spellOut
|
||||||
|
formatter.allowedUnits = [.hour, .minute, .day]
|
||||||
|
|
||||||
|
|
||||||
|
let durationString = formatter.string(from: duration)!
|
||||||
|
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
|
||||||
|
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
|
||||||
|
guard success else { return }
|
||||||
|
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
|
||||||
|
persistedAuthenticationContexts[secret] = context
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ SecretKit is a collection of protocols describing secrets and stores.
|
|||||||
|
|
||||||
### Authentication Persistence
|
### Authentication Persistence
|
||||||
|
|
||||||
- ``AuthenticationContextProtocol``
|
- ``PersistedAuthenticationContext``
|
||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalAuthentication
|
|
||||||
|
|
||||||
/// Type eraser for SecretStore.
|
/// Type eraser for SecretStore.
|
||||||
open class AnySecretStore: SecretStore, @unchecked Sendable {
|
open class AnySecretStore: SecretStore, @unchecked Sendable {
|
||||||
@@ -9,7 +8,9 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
private let _id: @Sendable () -> UUID
|
private let _id: @Sendable () -> UUID
|
||||||
private let _name: @MainActor @Sendable () -> String
|
private let _name: @MainActor @Sendable () -> String
|
||||||
private let _secrets: @MainActor @Sendable () -> [AnySecret]
|
private let _secrets: @MainActor @Sendable () -> [AnySecret]
|
||||||
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance, SigningRequestTarget?, LAContext?) async throws -> Data
|
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance, SigningRequestTarget?) async throws -> Data
|
||||||
|
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext?
|
||||||
|
private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void
|
||||||
private let _reloadSecrets: @Sendable () async -> Void
|
private let _reloadSecrets: @Sendable () async -> Void
|
||||||
|
|
||||||
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
||||||
@@ -18,7 +19,9 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
_name = { secretStore.name }
|
_name = { secretStore.name }
|
||||||
_id = { secretStore.id }
|
_id = { secretStore.id }
|
||||||
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
||||||
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2, target: $3, context: $4) }
|
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2, target: $3) }
|
||||||
|
_existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
|
||||||
|
_persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
||||||
_reloadSecrets = { await secretStore.reloadSecrets() }
|
_reloadSecrets = { await secretStore.reloadSecrets() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +41,16 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
return _secrets()
|
return _secrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?, context: LAContext?) async throws -> Data {
|
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data {
|
||||||
try await _sign(data, secret, provenance, target, context)
|
try await _sign(data, secret, provenance, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? {
|
||||||
|
await _existingPersistedAuthenticationContext(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) async throws {
|
||||||
|
try await _persistAuthentication(secret, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reloadSecrets() async {
|
public func reloadSecrets() async {
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import LocalAuthentication
|
|
||||||
|
|
||||||
/// Protocol describing an authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time.
|
|
||||||
public protocol AuthenticationContextProtocol: Sendable, Identifiable {
|
|
||||||
var secret: AnySecret { get }
|
|
||||||
func valid(for request: SignatureRequest) -> Bool
|
|
||||||
var laContext: LAContext? { get }
|
|
||||||
func evaluate(requireBiometrics: Bool) async throws -> Bool
|
|
||||||
func cancel() async
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct SignatureRequest: Identifiable, Hashable, Sendable, Comparable {
|
|
||||||
|
|
||||||
public let id: UUID
|
|
||||||
public let date: Date
|
|
||||||
public let secret: AnySecret
|
|
||||||
public let provenance: SigningRequestProvenance
|
|
||||||
public let target: SigningRequestTarget?
|
|
||||||
|
|
||||||
public init(secret: AnySecret, provenance: SigningRequestProvenance, target: SigningRequestTarget?) {
|
|
||||||
self.id = UUID()
|
|
||||||
self.date = Date()
|
|
||||||
self.secret = secret
|
|
||||||
self.provenance = provenance
|
|
||||||
self.target = target
|
|
||||||
}
|
|
||||||
|
|
||||||
public var batchID: Int {
|
|
||||||
var hasher = Hasher()
|
|
||||||
guard let target else {
|
|
||||||
// Requests without target are not permitted to be batched.
|
|
||||||
id.hash(into: &hasher)
|
|
||||||
return hasher.finalize()
|
|
||||||
}
|
|
||||||
provenance.batchID.hash(into: &hasher)
|
|
||||||
target.batchID.hash(into: &hasher)
|
|
||||||
secret.id.hash(into: &hasher)
|
|
||||||
return hasher.finalize()
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func < (lhs: SignatureRequest, rhs: SignatureRequest) -> Bool {
|
|
||||||
lhs.date < rhs.date
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,9 @@ public struct Attributes: Sendable, Codable, Hashable {
|
|||||||
/// The authentication requirements for the key. This is simply a description of the option recorded at creation – modifying it doers not modify the key's authentication requirements.
|
/// The authentication requirements for the key. This is simply a description of the option recorded at creation – modifying it doers not modify the key's authentication requirements.
|
||||||
public let authentication: AuthenticationRequirement
|
public let authentication: AuthenticationRequirement
|
||||||
|
|
||||||
|
/// The authentication restrictions for the key.
|
||||||
|
public var restrictions: Restrictions?
|
||||||
|
|
||||||
/// The string appended to the end of the SSH Public Key.
|
/// The string appended to the end of the SSH Public Key.
|
||||||
/// If nil, a default value will be used.
|
/// If nil, a default value will be used.
|
||||||
public var publicKeyAttribution: String?
|
public var publicKeyAttribution: String?
|
||||||
@@ -15,10 +18,12 @@ public struct Attributes: Sendable, Codable, Hashable {
|
|||||||
public init(
|
public init(
|
||||||
keyType: KeyType,
|
keyType: KeyType,
|
||||||
authentication: AuthenticationRequirement,
|
authentication: AuthenticationRequirement,
|
||||||
|
restrictions: Restrictions?,
|
||||||
publicKeyAttribution: String? = nil
|
publicKeyAttribution: String? = nil
|
||||||
) {
|
) {
|
||||||
self.keyType = keyType
|
self.keyType = keyType
|
||||||
self.authentication = authentication
|
self.authentication = authentication
|
||||||
|
self.restrictions = restrictions
|
||||||
self.publicKeyAttribution = publicKeyAttribution
|
self.publicKeyAttribution = publicKeyAttribution
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,3 +58,46 @@ public enum AuthenticationRequirement: String, Hashable, Sendable, Codable, Iden
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The restrictions for the key.
|
||||||
|
public struct Restrictions: Hashable, Sendable, Codable, Identifiable {
|
||||||
|
|
||||||
|
public enum AllowedDomains: Hashable, Sendable, Codable {
|
||||||
|
case all
|
||||||
|
case specific([String])
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .all:
|
||||||
|
break
|
||||||
|
case .specific(let values):
|
||||||
|
values.hash(into: &hasher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AllowedProvenancePaths: Hashable, Sendable, Codable, Identifiable {
|
||||||
|
case all
|
||||||
|
case specific([String])
|
||||||
|
|
||||||
|
public var id: Int {
|
||||||
|
hashValue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public var allowForwarding: Bool
|
||||||
|
public var allowSigning: Bool
|
||||||
|
public var allowConnections: Bool
|
||||||
|
public var allowedDomains: AllowedDomains
|
||||||
|
public var allowedProvenancePaths: AllowedProvenancePaths
|
||||||
|
|
||||||
|
public var id: Restrictions {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// public static let `default` = Restrictions(allowForwarding: true, allowSigning: true, allowConnections: true, allowedDomains: .all, allowedProvenancePaths: .all)
|
||||||
|
public static let `default` = Restrictions(allowForwarding: true, allowSigning: true, allowConnections: true, allowedDomains: .specific(["example.com"]), allowedProvenancePaths: .all)
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Protocol describing a persisted authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time.
|
||||||
|
public protocol PersistedAuthenticationContext: Sendable {
|
||||||
|
/// Whether the context remains valid.
|
||||||
|
var valid: Bool { get }
|
||||||
|
/// The date at which the authorization expires and the context becomes invalid.
|
||||||
|
var expiration: Date { get }
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalAuthentication
|
|
||||||
|
|
||||||
/// Manages access to Secrets, and performs signature operations on data using those Secrets.
|
/// Manages access to Secrets, and performs signature operations on data using those Secrets.
|
||||||
public protocol SecretStore<SecretType>: Identifiable, Sendable {
|
public protocol SecretStore<SecretType>: Identifiable, Sendable {
|
||||||
@@ -21,7 +20,20 @@ public protocol SecretStore<SecretType>: Identifiable, Sendable {
|
|||||||
/// - secret: The ``Secret`` to sign with.
|
/// - secret: The ``Secret`` to sign with.
|
||||||
/// - provenance: A ``SigningRequestProvenance`` describing where the request came from.
|
/// - provenance: A ``SigningRequestProvenance`` describing where the request came from.
|
||||||
/// - Returns: The signed data.
|
/// - Returns: The signed data.
|
||||||
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance, target: SigningRequestTarget?, context: LAContext?) async throws -> Data
|
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data
|
||||||
|
|
||||||
|
/// Checks to see if there is currently a valid persisted authentication for a given secret.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
|
||||||
|
/// - Returns: A persisted authentication context, if a valid one exists.
|
||||||
|
func existingPersistedAuthenticationContext(secret: SecretType) async -> PersistedAuthenticationContext?
|
||||||
|
|
||||||
|
/// Persists user authorization for access to a secret.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - secret: The ``Secret`` to persist the authorization for.
|
||||||
|
/// - duration: The duration that the authorization should persist for.
|
||||||
|
/// - Note: This is used for temporarily unlocking access to a secret which would otherwise require authentication every single use. This is useful for situations where the user anticipates several rapid accesses to a authorization-guarded secret.
|
||||||
|
func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws
|
||||||
|
|
||||||
/// Requests that the store reload secrets from any backing store, if neccessary.
|
/// Requests that the store reload secrets from any backing store, if neccessary.
|
||||||
func reloadSecrets() async
|
func reloadSecrets() async
|
||||||
|
|||||||
@@ -2,23 +2,13 @@ import Foundation
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
/// Describes the chain of applications that requested a signature operation.
|
/// Describes the chain of applications that requested a signature operation.
|
||||||
public struct SigningRequestProvenance: Hashable, Sendable {
|
public struct SigningRequestProvenance: Equatable, Sendable {
|
||||||
|
|
||||||
/// A list of processes involved in the request.
|
/// A list of processes involved in the request.
|
||||||
/// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app`
|
/// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app`
|
||||||
public var chain: [Process]
|
public var chain: [Process]
|
||||||
|
public init(root: Process) {
|
||||||
public var date: Date
|
|
||||||
|
|
||||||
public init(root: Process, date: Date = .now) {
|
|
||||||
self.chain = [root]
|
self.chain = [root]
|
||||||
self.date = date
|
|
||||||
}
|
|
||||||
|
|
||||||
public var batchID: Int {
|
|
||||||
var hasher = Hasher()
|
|
||||||
chain.map(\.path).hash(into: &hasher)
|
|
||||||
return hasher.finalize()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -40,7 +30,7 @@ extension SigningRequestProvenance {
|
|||||||
extension SigningRequestProvenance {
|
extension SigningRequestProvenance {
|
||||||
|
|
||||||
/// Describes a process in a `SigningRequestProvenance` chain.
|
/// Describes a process in a `SigningRequestProvenance` chain.
|
||||||
public struct Process: Hashable, Sendable {
|
public struct Process: Equatable, Sendable {
|
||||||
|
|
||||||
/// The pid of the process.
|
/// The pid of the process.
|
||||||
public let pid: Int32
|
public let pid: Int32
|
||||||
|
|||||||
@@ -2,17 +2,12 @@ import Foundation
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
/// Describes the target of the signature operation.
|
/// Describes the target of the signature operation.
|
||||||
public enum SigningRequestTarget: Sendable, Hashable {
|
public enum SigningRequestTarget: Sendable {
|
||||||
|
|
||||||
case connection(ConnectionPayload)
|
case connection(ConnectionPayload)
|
||||||
case signature(SignaturePayload)
|
case signature(SignaturePayload)
|
||||||
|
|
||||||
public var batchID: Int {
|
public struct ConnectionPayload: Sendable, Codable{
|
||||||
hashValue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public struct ConnectionPayload: Sendable, Codable, Hashable {
|
|
||||||
|
|
||||||
public let username: String
|
public let username: String
|
||||||
public let hasSignature: Bool
|
public let hasSignature: Bool
|
||||||
@@ -39,7 +34,7 @@ public enum SigningRequestTarget: Sendable, Hashable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SignaturePayload: Sendable, Codable, Hashable {
|
public struct SignaturePayload: Sendable, Codable {
|
||||||
|
|
||||||
public let namespace: String
|
public let namespace: String
|
||||||
public let hashAlgorithm: String
|
public let hashAlgorithm: String
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Security
|
|||||||
import CryptoTokenKit
|
import CryptoTokenKit
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import OSLog
|
import os
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ extension SecureEnclave {
|
|||||||
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
|
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
|
||||||
do {
|
do {
|
||||||
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
|
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
|
||||||
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
|
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth, restrictions: .default))
|
||||||
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
||||||
logger.log("Skipping \(name), public key already present. Marking as migrated.")
|
logger.log("Skipping \(name), public key already present. Marking as migrated.")
|
||||||
markMigrated(secret: secret, oldID: id)
|
markMigrated(secret: secret, oldID: id)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Security
|
|||||||
import CryptoKit
|
import CryptoKit
|
||||||
import LocalAuthentication
|
import LocalAuthentication
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import OSLog
|
import os
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ extension SecureEnclave {
|
|||||||
}
|
}
|
||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
public let name = String(localized: .secureEnclave)
|
public let name = String(localized: .secureEnclave)
|
||||||
|
private let persistentAuthenticationHandler = PersistentAuthenticationHandler<Secret>()
|
||||||
|
|
||||||
/// Initializes a Store.
|
/// Initializes a Store.
|
||||||
@MainActor public init() {
|
@MainActor public init() {
|
||||||
@@ -36,7 +37,24 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
// MARK: SecretStore
|
// MARK: SecretStore
|
||||||
|
|
||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?, context: LAContext?) async throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data {
|
||||||
|
var context: LAContext
|
||||||
|
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
|
||||||
|
context = unsafe existing.context
|
||||||
|
} else {
|
||||||
|
let newContext = LAContext()
|
||||||
|
switch target {
|
||||||
|
case .connection(let payload) where payload.host != nil:
|
||||||
|
newContext.localizedReason = String(localized: .authContextRequestSignatureForConnectionDescription(appName: provenance.origin.displayName, targetName: payload.host!, secretName: secret.name))
|
||||||
|
case .signature(let payload):
|
||||||
|
newContext.localizedReason = String(localized: .authContextRequestSignatureForSignatureDescription(namespace: payload.namespace, appName: provenance.origin.displayName, secretName: secret.name))
|
||||||
|
default:
|
||||||
|
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
|
||||||
|
}
|
||||||
|
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
||||||
|
context = newContext
|
||||||
|
}
|
||||||
|
|
||||||
let queryAttributes = KeychainDictionary([
|
let queryAttributes = KeychainDictionary([
|
||||||
kSecClass: Constants.keyClass,
|
kSecClass: Constants.keyClass,
|
||||||
kSecAttrService: Constants.keyTag,
|
kSecAttrService: Constants.keyTag,
|
||||||
@@ -77,6 +95,14 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
|
||||||
|
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
|
||||||
|
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor public func reloadSecrets() {
|
@MainActor public func reloadSecrets() {
|
||||||
let before = secrets
|
let before = secrets
|
||||||
secrets.removeAll()
|
secrets.removeAll()
|
||||||
@@ -157,7 +183,6 @@ extension SecureEnclave {
|
|||||||
let attributes = try JSONEncoder().encode(attributes)
|
let attributes = try JSONEncoder().encode(attributes)
|
||||||
let updatedAttributes = KeychainDictionary([
|
let updatedAttributes = KeychainDictionary([
|
||||||
kSecAttrLabel: name,
|
kSecAttrLabel: name,
|
||||||
kSecAttrService: Constants.keyTag,
|
|
||||||
kSecAttrGeneric: attributes,
|
kSecAttrGeneric: attributes,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -269,7 +294,7 @@ extension SecureEnclave.Store {
|
|||||||
|
|
||||||
enum Constants {
|
enum Constants {
|
||||||
static let keyClass = kSecClassGenericPassword as String
|
static let keyClass = kSecClassGenericPassword as String
|
||||||
static let keyTag = "com.maxgoedjen.secretive.secureenclave.key"
|
static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8)
|
||||||
static let notificationToken = UUID().uuidString
|
static let notificationToken = UUID().uuidString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
struct RequireDestinationInformationSettingsKey: SettingsStore.SettingsKey {
|
|
||||||
static let defaultValue: Bool = true
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SettingsStore {
|
|
||||||
|
|
||||||
public var requireDestinationInformation: Bool {
|
|
||||||
get { self[RequireDestinationInformationSettingsKey.self] }
|
|
||||||
set { self[RequireDestinationInformationSettingsKey.self] = newValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Observation
|
|
||||||
import Security
|
|
||||||
import OSLog
|
|
||||||
|
|
||||||
// Setting store backed by macOS keychain for stronger guarantees around ownership/other-process-modification than UserDefaults offers.
|
|
||||||
@Observable @MainActor public final class SettingsStore: Sendable {
|
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.settings", category: "SettingsStore")
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
private var state: [ObjectIdentifier: UUID] = [:]
|
|
||||||
|
|
||||||
subscript<SettingsKeyType: SettingsKey>(_ key: SettingsKeyType.Type) -> SettingsKeyType.Value {
|
|
||||||
get {
|
|
||||||
_ = state[ObjectIdentifier(key)]
|
|
||||||
let queryAttributes = KeychainDictionary([
|
|
||||||
kSecClass: Constants.keyClass,
|
|
||||||
kSecAttrService: Constants.keyTag,
|
|
||||||
kSecAttrAccount: String(describing: SettingsKeyType.self),
|
|
||||||
kSecUseDataProtectionKeychain: true,
|
|
||||||
kSecReturnData: true,
|
|
||||||
kSecReturnAttributes: true,
|
|
||||||
kSecMatchLimit: kSecMatchLimitOne,
|
|
||||||
])
|
|
||||||
var untyped: CFTypeRef?
|
|
||||||
unsafe SecItemCopyMatching(queryAttributes, &untyped)
|
|
||||||
guard let typed = untyped as? [CFString: Any] else { return SettingsKeyType.defaultValue }
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
guard let data = typed[kSecValueData] as? Data else { return SettingsKeyType.defaultValue }
|
|
||||||
return (try? decoder.decode(SettingValue<SettingsKeyType.Value>.self, from: data).value) ?? SettingsKeyType.defaultValue
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
do {
|
|
||||||
let data = try JSONEncoder().encode(SettingValue(value: newValue))
|
|
||||||
let keychainAttributes = KeychainDictionary([
|
|
||||||
kSecClass: Constants.keyClass,
|
|
||||||
kSecAttrService: Constants.keyTag,
|
|
||||||
kSecAttrAccount: String(describing: SettingsKeyType.self),
|
|
||||||
kSecUseDataProtectionKeychain: true,
|
|
||||||
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
||||||
kSecValueData: data,
|
|
||||||
])
|
|
||||||
let status = SecItemAdd(keychainAttributes, nil)
|
|
||||||
switch status {
|
|
||||||
case errSecSuccess:
|
|
||||||
break
|
|
||||||
case errSecDuplicateItem:
|
|
||||||
let updateQuery = KeychainDictionary([
|
|
||||||
kSecClass: Constants.keyClass,
|
|
||||||
kSecAttrService: Constants.keyTag,
|
|
||||||
kSecAttrAccount: String(describing: SettingsKeyType.self),
|
|
||||||
])
|
|
||||||
let updatedAttributes = KeychainDictionary([
|
|
||||||
kSecValueData: data,
|
|
||||||
])
|
|
||||||
let status = SecItemUpdate(updateQuery, updatedAttributes)
|
|
||||||
if status != errSecSuccess {
|
|
||||||
throw KeychainError(statusCode: status)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw KeychainError(statusCode: status)
|
|
||||||
}
|
|
||||||
state[ObjectIdentifier(key)] = UUID()
|
|
||||||
} catch {
|
|
||||||
logger.error("Error updating key: \(String(describing: SettingsKeyType.self), privacy: .public): \(error.localizedDescription.debugDescription, privacy: .public)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SettingsStore {
|
|
||||||
|
|
||||||
public protocol SettingsKey<Value>: Equatable, Codable, Sendable {
|
|
||||||
associatedtype Value: Codable
|
|
||||||
static var defaultValue: Value { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SettingsStore {
|
|
||||||
|
|
||||||
struct SettingValue<Value: Codable>: Codable {
|
|
||||||
let value: Value
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension SettingsStore {
|
|
||||||
|
|
||||||
fileprivate struct KeychainError: Error {
|
|
||||||
let statusCode: OSStatus?
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary {
|
|
||||||
dictionary as CFDictionary
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension SettingsStore {
|
|
||||||
|
|
||||||
enum Constants {
|
|
||||||
static let keyClass = kSecClassGenericPassword as String
|
|
||||||
static let keyTag = "com.maxgoedjen.settingsStore"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import Foundation
|
|||||||
import Security
|
import Security
|
||||||
import CryptoTokenKit
|
import CryptoTokenKit
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import OSLog
|
import os
|
||||||
import SSHProtocolKit
|
import SSHProtocolKit
|
||||||
import CertificateKit
|
import CertificateKit
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ extension SmartCard {
|
|||||||
public let name: String
|
public let name: String
|
||||||
public let publicKey: Data
|
public let publicKey: Data
|
||||||
public var attributes: Attributes
|
public var attributes: Attributes
|
||||||
|
public var restrictions: Restrictions?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ extension SmartCard {
|
|||||||
public var secrets: [Secret] {
|
public var secrets: [Secret] {
|
||||||
state.secrets
|
state.secrets
|
||||||
}
|
}
|
||||||
|
private let persistentAuthenticationHandler = PersistentAuthenticationHandler<Secret>()
|
||||||
|
|
||||||
/// Initializes a Store.
|
/// Initializes a Store.
|
||||||
public init() {
|
public init() {
|
||||||
@@ -56,19 +57,25 @@ extension SmartCard {
|
|||||||
|
|
||||||
// MARK: Public API
|
// MARK: Public API
|
||||||
|
|
||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?, context: LAContext?) async throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) async throws -> Data {
|
||||||
guard let tokenID = await state.tokenID else { fatalError() }
|
guard let tokenID = await state.tokenID else { fatalError() }
|
||||||
var rawAttributes: [CFString: Any] = [
|
var context: LAContext
|
||||||
|
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
|
||||||
|
context = unsafe existing.context
|
||||||
|
} else {
|
||||||
|
let newContext = LAContext()
|
||||||
|
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
|
||||||
|
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
||||||
|
context = newContext
|
||||||
|
}
|
||||||
|
let attributes = KeychainDictionary([
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
kSecAttrApplicationLabel: secret.id as CFData,
|
kSecAttrApplicationLabel: secret.id as CFData,
|
||||||
kSecAttrTokenID: tokenID,
|
kSecAttrTokenID: tokenID,
|
||||||
|
kSecUseAuthenticationContext: context,
|
||||||
kSecReturnRef: true
|
kSecReturnRef: true
|
||||||
]
|
])
|
||||||
if let context {
|
|
||||||
rawAttributes[kSecUseAuthenticationContext] = context
|
|
||||||
}
|
|
||||||
let attributes = KeychainDictionary(rawAttributes)
|
|
||||||
var untyped: CFTypeRef?
|
var untyped: CFTypeRef?
|
||||||
let status = unsafe SecItemCopyMatching(attributes, &untyped)
|
let status = unsafe SecItemCopyMatching(attributes, &untyped)
|
||||||
if status != errSecSuccess {
|
if status != errSecSuccess {
|
||||||
@@ -86,6 +93,14 @@ extension SmartCard {
|
|||||||
return signature as Data
|
return signature as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
|
||||||
|
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
|
||||||
|
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
/// Reloads all secrets from the store.
|
/// Reloads all secrets from the store.
|
||||||
@MainActor public func reloadSecrets() {
|
@MainActor public func reloadSecrets() {
|
||||||
reloadSecretsInternal()
|
reloadSecretsInternal()
|
||||||
@@ -156,7 +171,7 @@ extension SmartCard.Store {
|
|||||||
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
|
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
|
||||||
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
|
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
|
||||||
let publicKey = publicKeyAttributes[kSecValueData] as! Data
|
let publicKey = publicKeyAttributes[kSecValueData] as! Data
|
||||||
let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, authentication: .presenceRequired)
|
let attributes = Attributes(keyType: KeyType(secAttr: algorithmSecAttr, size: keySize)!, authentication: .presenceRequired, restrictions: .default)
|
||||||
let secret = SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes)
|
let secret = SmartCard.Secret(id: tokenID, name: name, publicKey: publicKey, attributes: attributes)
|
||||||
guard signatureAlgorithm(for: secret) != nil else { return nil }
|
guard signatureAlgorithm(for: secret) != nil else { return nil }
|
||||||
return secret
|
return secret
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension ProcessInfo {
|
||||||
|
private static let fallbackTeamID = "Z72PRUAWF6"
|
||||||
|
|
||||||
|
private static let teamID: String = {
|
||||||
|
#if DEBUG
|
||||||
|
guard let task = SecTaskCreateFromSelf(nil) else {
|
||||||
|
assertionFailure("SecTaskCreateFromSelf failed")
|
||||||
|
return fallbackTeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let value = SecTaskCopyValueForEntitlement(task, "com.apple.developer.team-identifier" as CFString, nil) as? String else {
|
||||||
|
// assertionFailure("SecTaskCopyValueForEntitlement(com.apple.developer.team-identifier) failed")
|
||||||
|
return fallbackTeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
#else
|
||||||
|
/// Always use hardcoded team ID for release builds, just in case.
|
||||||
|
return fallbackTeamID
|
||||||
|
#endif
|
||||||
|
}()
|
||||||
|
|
||||||
|
public var teamID: String { Self.teamID }
|
||||||
|
}
|
||||||
@@ -12,9 +12,7 @@ public final class XPCServiceDelegate: NSObject, NSXPCListenerDelegate {
|
|||||||
newConnection.exportedInterface = NSXPCInterface(with: (any _XPCProtocol).self)
|
newConnection.exportedInterface = NSXPCInterface(with: (any _XPCProtocol).self)
|
||||||
let exportedObject = exportedObject
|
let exportedObject = exportedObject
|
||||||
newConnection.exportedObject = exportedObject
|
newConnection.exportedObject = exportedObject
|
||||||
#if !DEBUG
|
newConnection.setCodeSigningRequirement("anchor apple generic and certificate leaf[subject.OU] = \"\(ProcessInfo.processInfo.teamID)\"")
|
||||||
newConnection.setCodeSigningRequirement("anchor apple generic and certificate leaf[subject.OU] = \"Z72PRUAWF6\"")
|
|
||||||
#endif
|
|
||||||
newConnection.resume()
|
newConnection.resume()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ public struct XPCTypedSession<ResponseType: Codable & Sendable, ErrorType: Error
|
|||||||
public init(serviceName: String, warmup: Bool = false) async throws {
|
public init(serviceName: String, warmup: Bool = false) async throws {
|
||||||
let connection = NSXPCConnection(serviceName: serviceName)
|
let connection = NSXPCConnection(serviceName: serviceName)
|
||||||
connection.remoteObjectInterface = NSXPCInterface(with: (any _XPCProtocol).self)
|
connection.remoteObjectInterface = NSXPCInterface(with: (any _XPCProtocol).self)
|
||||||
#if !DEBUG
|
connection.setCodeSigningRequirement("anchor apple generic and certificate leaf[subject.OU] = \"\(ProcessInfo.processInfo.teamID)\"")
|
||||||
connection.setCodeSigningRequirement("anchor apple generic and certificate leaf[subject.OU] = \"Z72PRUAWF6\"")
|
|
||||||
#endif
|
|
||||||
connection.resume()
|
connection.resume()
|
||||||
guard let proxy = connection.remoteObjectProxy as? _XPCProtocol else { fatalError() }
|
guard let proxy = connection.remoteObjectProxy as? _XPCProtocol else { fatalError() }
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ import CertificateKit
|
|||||||
// MARK: Identity Listing
|
// MARK: Identity Listing
|
||||||
|
|
||||||
@Test func emptyStores() async throws {
|
@Test func emptyStores() async throws {
|
||||||
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
|
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, hosts: nil)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestIdentitiesEmpty)
|
#expect(response == Constants.Responses.requestIdentitiesEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@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, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
|
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, hosts: nil)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
|
|
||||||
let actual = OpenSSHReader(data: response)
|
let actual = OpenSSHReader(data: response)
|
||||||
let expected = OpenSSHReader(data: Constants.Responses.requestIdentitiesMultiple)
|
let expected = OpenSSHReader(data: Constants.Responses.requestIdentitiesMultiple)
|
||||||
@@ -33,9 +33,9 @@ import CertificateKit
|
|||||||
|
|
||||||
@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, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
|
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, hosts: nil)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestFailure)
|
#expect(response == Constants.Responses.requestFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ import CertificateKit
|
|||||||
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, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
|
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||||
let response = await agent.handle(request: request, provenance: .test, hosts: nil)
|
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)
|
||||||
let type = try responseReader.readNextBytes(as: UInt8.self)
|
let type = try responseReader.readNextBytes(as: UInt8.self)
|
||||||
@@ -68,7 +68,7 @@ import CertificateKit
|
|||||||
let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
|
let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
|
||||||
// Correct signature
|
// Correct signature
|
||||||
#expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
|
#expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
|
||||||
.isValidSignature(signature, for: context.dataToSign.raw))
|
.isValidSignature(signature, for: context.dataToSign))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Witness protocol
|
// MARK: Witness protocol
|
||||||
@@ -78,8 +78,8 @@ import CertificateKit
|
|||||||
let witness = StubWitness(speakNow: { _,_ in
|
let witness = StubWitness(speakNow: { _,_ in
|
||||||
return true
|
return true
|
||||||
}, witness: { _, _ in })
|
}, witness: { _, _ in })
|
||||||
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), witness: witness)
|
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
|
||||||
let response = await agent.handle(request: .signRequest(.empty), provenance: .test, hosts: nil)
|
let response = await agent.handle(request: .signRequest(.empty), provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestFailure)
|
#expect(response == Constants.Responses.requestFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +91,9 @@ import CertificateKit
|
|||||||
}, witness: { _, trace in
|
}, witness: { _, trace in
|
||||||
witnessed = true
|
witnessed = true
|
||||||
})
|
})
|
||||||
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), 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, hosts: nil)
|
_ = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(witnessed)
|
#expect(witnessed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,9 +107,9 @@ import CertificateKit
|
|||||||
}, witness: { _, trace in
|
}, witness: { _, trace in
|
||||||
witnessTrace = trace
|
witnessTrace = trace
|
||||||
})
|
})
|
||||||
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), 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, hosts: nil)
|
_ = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(witnessTrace == speakNowTrace)
|
#expect(witnessTrace == speakNowTrace)
|
||||||
#expect(witnessTrace == .test)
|
#expect(witnessTrace == .test)
|
||||||
}
|
}
|
||||||
@@ -120,18 +120,57 @@ import CertificateKit
|
|||||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
let store = 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, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
|
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, hosts: nil)
|
let response = await agent.handle(request: request, provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestFailure)
|
#expect(response == Constants.Responses.requestFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Unsupported
|
// MARK: Unsupported
|
||||||
|
|
||||||
@Test func unhandledAdd() async throws {
|
@Test func unhandledAdd() async throws {
|
||||||
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
|
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore())
|
||||||
let response = await agent.handle(request: .addIdentity, provenance: .test, hosts: nil)
|
let response = await agent.handle(request: .addIdentity, provenance: .test)
|
||||||
#expect(response == Constants.Responses.requestFailure)
|
#expect(response == Constants.Responses.requestFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SigningRequestProvenance {
|
||||||
|
|
||||||
|
static let test = SigningRequestProvenance(root: .init(pid: 0, processName: "test", appName: nil, iconURL: nil, path: "/", validSignature: true, parentPID: 0))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AgentTests {
|
||||||
|
|
||||||
|
func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList {
|
||||||
|
let store = Stub.Store()
|
||||||
|
store.secrets.append(contentsOf: secrets)
|
||||||
|
let storeList = SecretStoreList()
|
||||||
|
storeList.add(store: store)
|
||||||
|
return storeList
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
|
||||||
|
enum Requests {
|
||||||
|
static let requestIdentities = Data(base64Encoded: "AAAAAQs=")!
|
||||||
|
static let requestSignatureWithNoneMatching = Data(base64Encoded: "AAABhA0AAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAO8AAAAgbqmrqPUtJ8mmrtaSVexjMYyXWNqjHSnoto7zgv86xvcyAAAAA2dpdAAAAA5zc2gtY29ubmVjdGlvbgAAAAlwdWJsaWNrZXkBAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAAA=")!
|
||||||
|
static let requestSignature = Data(base64Encoded: "AAABRA0AAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08AAADPAAAAIBIFsbCZ4/dhBmLNGHm0GKj7EJ4N8k/jXRxlyg+LFIYzMgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAAA==")!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Responses {
|
||||||
|
static let requestIdentitiesEmpty = Data(base64Encoded: "AAAABQwAAAAA")!
|
||||||
|
static let requestIdentitiesMultiple = Data(base64Encoded: "AAABLwwAAAACAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAFWVjZHNhLTI1NkBleGFtcGxlLmNvbQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAGEEspLMDmreMJverQkqKC9zF9ZUasn5uSWkbRlz1jNTCtuyH1KKm+VImL6wdAj47SbzwM6lEEC24AdfrR64P9i/bnS2i83v/4wQVtcZn+Et13QGgWlZst8lxCPzTookaVwMAAAAFWVjZHNhLTM4NEBleGFtcGxlLmNvbQ==")!
|
||||||
|
static let requestFailure = Data(base64Encoded: "AAAAAQU=")!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Secrets {
|
||||||
|
static let ecdsa256Secret = Stub.Secret(keySize: 256, publicKey: Data(base64Encoded: "BKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08=")!, privateKey: Data(base64Encoded: "BKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy09nw780wy/TSfUmzj15iJkV234AaCLNl+H8qFL6qK8VIg==")!)
|
||||||
|
static let ecdsa384Secret = Stub.Secret(keySize: 384, publicKey: Data(base64Encoded: "BLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDA==")!, privateKey: Data(base64Encoded: "BLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDHNapAOzrt9E+9QC4/KYoXS7Uw4pmdAz53uIj02tttiq3c0ZyIQ7XoscWWRqRrz8Kw==")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import Testing
|
|
||||||
import Foundation
|
|
||||||
import LocalAuthentication
|
|
||||||
import SecretKit
|
|
||||||
import SecretAgentKit
|
|
||||||
import SSHProtocolKit
|
|
||||||
|
|
||||||
public struct TestSecret: Secret {
|
|
||||||
|
|
||||||
public let id = UUID()
|
|
||||||
public let name = "Test"
|
|
||||||
public let publicKey = Data()
|
|
||||||
public var attributes: Attributes
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor final class TestContext: @preconcurrency AuthenticationContextProtocol, Sendable {
|
|
||||||
|
|
||||||
let id = UUID()
|
|
||||||
private var testEvaluationResult: Bool
|
|
||||||
|
|
||||||
private(set) var canceled: Bool
|
|
||||||
private(set) var evaluationResult: Bool? = nil
|
|
||||||
|
|
||||||
let approval: ApprovalMode
|
|
||||||
let secret: AnySecret
|
|
||||||
let laContext: LAContext? = nil
|
|
||||||
|
|
||||||
enum ApprovalMode {
|
|
||||||
case manual
|
|
||||||
case duration(Duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(authenticationRequirement: AuthenticationRequirement, testEvaluationResult: Bool, approval: ApprovalMode = .manual) {
|
|
||||||
self.canceled = false
|
|
||||||
self.testEvaluationResult = testEvaluationResult
|
|
||||||
self.secret = AnySecret(TestSecret(attributes: .init(keyType: .ecdsa256, authentication: authenticationRequirement)))
|
|
||||||
self.approval = approval
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated func valid(for request: SecretKit.SignatureRequest) -> Bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
func evaluate(requireBiometrics: Bool) async throws -> Bool {
|
|
||||||
if case let .duration(duration) = approval {
|
|
||||||
try await Task.sleep(for: duration)
|
|
||||||
} else {
|
|
||||||
while true {
|
|
||||||
try await Task.sleep(for: .milliseconds(10))
|
|
||||||
guard !canceled else { return false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guard !canceled else { return false }
|
|
||||||
evaluationResult = testEvaluationResult
|
|
||||||
return testEvaluationResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancel() async {
|
|
||||||
assert(!canceled)
|
|
||||||
assert(evaluationResult == nil)
|
|
||||||
canceled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor @Suite struct AuthenticationHandlerTests {
|
|
||||||
|
|
||||||
let handler = AuthenticationHandler()
|
|
||||||
|
|
||||||
@Test func singleImmediatelyRequests() async throws {
|
|
||||||
var calledPresentPending = false
|
|
||||||
handler.setPendingRequestHandler {
|
|
||||||
calledPresentPending = true
|
|
||||||
}
|
|
||||||
let context = TestContext(authenticationRequirement: .presenceRequired, testEvaluationResult: true, approval: .duration(.zero))
|
|
||||||
_ = try await handler.authenticatedContext(for: .init(secret: .init(context.secret), provenance: .test, target: nil), context: context)
|
|
||||||
#expect(context.evaluationResult == true)
|
|
||||||
#expect(!calledPresentPending)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func sequentialRequestsDoNotBlock() async throws {
|
|
||||||
var calledPresentPending = false
|
|
||||||
handler.setPendingRequestHandler {
|
|
||||||
calledPresentPending = true
|
|
||||||
}
|
|
||||||
let context = TestContext(authenticationRequirement: .presenceRequired, testEvaluationResult: true, approval: .duration(.milliseconds(1)))
|
|
||||||
_ = try await handler.authenticatedContext(for: .init(secret: .init(context.secret), provenance: .test, target: nil), context: context)
|
|
||||||
#expect(context.evaluationResult == true)
|
|
||||||
let second = TestContext(authenticationRequirement: .presenceRequired, testEvaluationResult: true, approval: .duration(.milliseconds(1)))
|
|
||||||
_ = try await handler.authenticatedContext(for: .init(secret: .init(second.secret), provenance: .test, target: nil), context: second)
|
|
||||||
#expect(context.evaluationResult == true)
|
|
||||||
#expect(second.evaluationResult == true)
|
|
||||||
#expect(!calledPresentPending)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func secondRetractsAndPresentsBatch() async throws {
|
|
||||||
var calledPresentPending = false
|
|
||||||
handler.setPendingRequestHandler {
|
|
||||||
calledPresentPending = true
|
|
||||||
}
|
|
||||||
let contextA = TestContext(authenticationRequirement: .presenceRequired, testEvaluationResult: true)
|
|
||||||
let contextB = TestContext(authenticationRequirement: .presenceRequired, testEvaluationResult: true)
|
|
||||||
Task {
|
|
||||||
_ = try? await handler.authenticatedContext(for: .init(secret: .init(contextA.secret), provenance: .test, target: nil), context: contextA)
|
|
||||||
}
|
|
||||||
Task {
|
|
||||||
_ = try? await handler.authenticatedContext(for: .init(secret: .init(contextB.secret), provenance: .test, target: nil), context: contextB)
|
|
||||||
}
|
|
||||||
#expect(contextA.evaluationResult == nil)
|
|
||||||
#expect(contextB.evaluationResult == nil)
|
|
||||||
await Task.yield()
|
|
||||||
#expect(contextA.canceled)
|
|
||||||
#expect(calledPresentPending)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func authRequiredDoesNotBlockNoAuthRequired() async throws {
|
|
||||||
var calledPresentPending = false
|
|
||||||
handler.setPendingRequestHandler {
|
|
||||||
calledPresentPending = true
|
|
||||||
}
|
|
||||||
let contextA = TestContext(authenticationRequirement: .presenceRequired, testEvaluationResult: true, approval: .manual)
|
|
||||||
let contextB = TestContext(authenticationRequirement: .notRequired, testEvaluationResult: true)
|
|
||||||
var unauthenticatedResult = false
|
|
||||||
Task {
|
|
||||||
Task {
|
|
||||||
_ = try? await handler.authenticatedContext(for: .init(secret: .init(contextB.secret), provenance: .test, target: nil), context: contextB)
|
|
||||||
unauthenticatedResult = true
|
|
||||||
}
|
|
||||||
_ = try? await handler.authenticatedContext(for: .init(secret: .init(contextA.secret), provenance: .test, target: nil), context: contextA)
|
|
||||||
}
|
|
||||||
try await Task.sleep(for: .milliseconds(10))
|
|
||||||
#expect(contextA.evaluationResult == nil)
|
|
||||||
#expect(unauthenticatedResult)
|
|
||||||
#expect(!contextA.canceled)
|
|
||||||
#expect(!contextB.canceled)
|
|
||||||
#expect(!calledPresentPending)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func batchDoesNotBlockNoAuthRequired() async throws {
|
|
||||||
var calledPresentPending = false
|
|
||||||
handler.setPendingRequestHandler {
|
|
||||||
calledPresentPending = true
|
|
||||||
}
|
|
||||||
let contextA = TestContext(authenticationRequirement: .presenceRequired, testEvaluationResult: true)
|
|
||||||
let contextB = TestContext(authenticationRequirement: .presenceRequired, testEvaluationResult: true)
|
|
||||||
let contextC = TestContext(authenticationRequirement: .notRequired, testEvaluationResult: true)
|
|
||||||
Task {
|
|
||||||
_ = try? await handler.authenticatedContext(for: .init(secret: .init(contextA.secret), provenance: .test, target: nil), context: contextA)
|
|
||||||
}
|
|
||||||
Task {
|
|
||||||
_ = try? await handler.authenticatedContext(for: .init(secret: .init(contextB.secret), provenance: .test, target: nil), context: contextB)
|
|
||||||
}
|
|
||||||
#expect(contextA.evaluationResult == nil)
|
|
||||||
#expect(contextB.evaluationResult == nil)
|
|
||||||
await Task.yield()
|
|
||||||
#expect(contextA.canceled)
|
|
||||||
#expect(calledPresentPending)
|
|
||||||
_ = try? await handler.authenticatedContext(for: .init(secret: .init(contextC.secret), provenance: .test, target: nil), context: contextA)
|
|
||||||
#expect(contextA.evaluationResult == nil)
|
|
||||||
#expect(contextB.evaluationResult == nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func batching() async throws {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import SecretKit
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension SigningRequestProvenance {
|
|
||||||
|
|
||||||
static let test = SigningRequestProvenance(root: .init(pid: 0, processName: "test", appName: nil, iconURL: nil, path: "/", validSignature: true, parentPID: 0))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AgentTests {
|
|
||||||
|
|
||||||
func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList {
|
|
||||||
let store = Stub.Store()
|
|
||||||
store.secrets.append(contentsOf: secrets)
|
|
||||||
let storeList = SecretStoreList()
|
|
||||||
storeList.add(store: store)
|
|
||||||
return storeList
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Constants {
|
|
||||||
|
|
||||||
enum Requests {
|
|
||||||
static let requestIdentities = Data(base64Encoded: "AAAAAQs=")!
|
|
||||||
static let requestSignatureWithNoneMatching = Data(base64Encoded: "AAABhA0AAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAO8AAAAgbqmrqPUtJ8mmrtaSVexjMYyXWNqjHSnoto7zgv86xvcyAAAAA2dpdAAAAA5zc2gtY29ubmVjdGlvbgAAAAlwdWJsaWNrZXkBAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAAA=")!
|
|
||||||
static let requestSignature = Data(base64Encoded: "AAABRA0AAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08AAADPAAAAIBIFsbCZ4/dhBmLNGHm0GKj7EJ4N8k/jXRxlyg+LFIYzMgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAAA==")!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Responses {
|
|
||||||
static let requestIdentitiesEmpty = Data(base64Encoded: "AAAABQwAAAAA")!
|
|
||||||
static let requestIdentitiesMultiple = Data(base64Encoded: "AAABLwwAAAACAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAFWVjZHNhLTI1NkBleGFtcGxlLmNvbQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAGEEspLMDmreMJverQkqKC9zF9ZUasn5uSWkbRlz1jNTCtuyH1KKm+VImL6wdAj47SbzwM6lEEC24AdfrR64P9i/bnS2i83v/4wQVtcZn+Et13QGgWlZst8lxCPzTookaVwMAAAAFWVjZHNhLTM4NEBleGFtcGxlLmNvbQ==")!
|
|
||||||
static let requestFailure = Data(base64Encoded: "AAAAAQU=")!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Secrets {
|
|
||||||
static let ecdsa256Secret = Stub.Secret(keySize: 256, publicKey: Data(base64Encoded: "BKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08=")!, privateKey: Data(base64Encoded: "BKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy09nw780wy/TSfUmzj15iJkV234AaCLNl+H8qFL6qK8VIg==")!)
|
|
||||||
static let ecdsa384Secret = Stub.Secret(keySize: 384, publicKey: Data(base64Encoded: "BLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDA==")!, privateKey: Data(base64Encoded: "BLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDHNapAOzrt9E+9QC4/KYoXS7Uw4pmdAz53uIj02tttiq3c0ZyIQ7XoscWWRqRrz8Kw==")!)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import Foundation
|
|||||||
import SecretKit
|
import SecretKit
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import SSHProtocolKit
|
import SSHProtocolKit
|
||||||
import LocalAuthentication
|
|
||||||
|
|
||||||
struct Stub {}
|
struct Stub {}
|
||||||
|
|
||||||
@@ -50,7 +49,7 @@ extension Stub {
|
|||||||
print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))")
|
print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?, context: LAContext?) throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws -> Data {
|
||||||
guard !shouldThrow else {
|
guard !shouldThrow else {
|
||||||
throw NSError(domain: "test", code: 0, userInfo: nil)
|
throw NSError(domain: "test", code: 0, userInfo: nil)
|
||||||
}
|
}
|
||||||
@@ -58,6 +57,13 @@ extension Stub {
|
|||||||
return try privateKey.signature(for: data).rawRepresentation
|
return try privateKey.signature(for: data).rawRepresentation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws {
|
||||||
|
}
|
||||||
|
|
||||||
public func reloadSecrets() {
|
public func reloadSecrets() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ struct StubWitness {
|
|||||||
|
|
||||||
extension StubWitness: SigningWitness {
|
extension StubWitness: SigningWitness {
|
||||||
|
|
||||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws {
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
|
||||||
let objection = speakNow(secret, provenance)
|
let objection = speakNow(secret, provenance)
|
||||||
if objection {
|
if objection {
|
||||||
throw TheresMyChance()
|
throw TheresMyChance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?, offerPersistence: Bool) async throws {
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
|
||||||
witness(secret, provenance)
|
witness(secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
import Cocoa
|
|
||||||
import OSLog
|
|
||||||
import SecretKit
|
|
||||||
import SecureEnclaveSecretKit
|
|
||||||
import SmartCardSecretKit
|
|
||||||
import SecretAgentKit
|
|
||||||
import Brief
|
|
||||||
import Observation
|
|
||||||
import Common
|
|
||||||
import SwiftUI
|
|
||||||
import CertificateKit
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct SecretAgent: App {
|
|
||||||
|
|
||||||
@MainActor private let storeList: SecretStoreList = {
|
|
||||||
let list = SecretStoreList()
|
|
||||||
let cryptoKit = SecureEnclave.Store()
|
|
||||||
let migrator = SecureEnclave.CryptoKitMigrator()
|
|
||||||
try? migrator.migrate(to: cryptoKit)
|
|
||||||
list.add(store: cryptoKit)
|
|
||||||
list.add(store: SmartCard.Store())
|
|
||||||
return list
|
|
||||||
}()
|
|
||||||
@MainActor private let certificateStore: CertificateStore = CertificateStore()
|
|
||||||
|
|
||||||
private let updater = Updater(checkOnLaunch: true)
|
|
||||||
private let notifier = Notifier()
|
|
||||||
private let authenticationHandler = AuthenticationHandler()
|
|
||||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory)
|
|
||||||
|
|
||||||
@Environment(\.openWindow) var openWindow
|
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "App")
|
|
||||||
@SceneBuilder var body: some Scene {
|
|
||||||
MenuBarExtra(isInserted: .constant(false)) {
|
|
||||||
EmptyView()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "lock")
|
|
||||||
.task {
|
|
||||||
await notifier.registerPersistenceHandler {
|
|
||||||
try await authenticationHandler.persistAuthentication(secret: $0, forDuration: $1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
let socketController = SocketController(path: URL.socketPath)
|
|
||||||
let agent = Agent(
|
|
||||||
storeList: storeList,
|
|
||||||
certificateStore: certificateStore,
|
|
||||||
authenticationHandler: authenticationHandler,
|
|
||||||
witness: notifier
|
|
||||||
)
|
|
||||||
for await session in socketController.sessions {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let inputParser = try await XPCAgentInputParser()
|
|
||||||
let hosts = try? await XPCHostsfileReader().read()
|
|
||||||
for await message in session.messages {
|
|
||||||
let request = try await inputParser.parse(data: message)
|
|
||||||
let agentResponse = await agent.handle(request: request, provenance: session.provenance, hosts: hosts)
|
|
||||||
try session.write(agentResponse)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
try? session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
|
||||||
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
|
|
||||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
let certsMigrator = CertificateMigrator(homeDirectory: URL.homeDirectory, certificateStore: certificateStore)
|
|
||||||
try? certsMigrator.migrate()
|
|
||||||
try? publicKeyFileStoreController.generateCertificates(for: certificateStore.certificates, clear: true)
|
|
||||||
for await _ in NotificationCenter.default.notifications(named: .certificateStoreReloaded) {
|
|
||||||
try? publicKeyFileStoreController.generateCertificates(for: certificateStore.certificates, clear: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
authenticationHandler.setPendingRequestHandler { @MainActor in
|
|
||||||
openWindow(value: PendingRequestsViewIdentifier())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
notifier.prompt()
|
|
||||||
_ = withObservationTracking {
|
|
||||||
updater.update
|
|
||||||
} onChange: { [updater, notifier] in
|
|
||||||
Task {
|
|
||||||
guard !updater.currentVersion.isTestBuild else { return }
|
|
||||||
await notifier.notify(update: updater.update!) { release in
|
|
||||||
await updater.ignore(release: release)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WindowGroup(for: PendingRequestsViewIdentifier.self) { _ in
|
|
||||||
pendingView
|
|
||||||
}
|
|
||||||
.windowStyle(.hiddenTitleBar)
|
|
||||||
.windowResizability(.contentSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
var pendingView: some View {
|
|
||||||
if !authenticationHandler.batchableRequests.isEmpty {
|
|
||||||
PendingRequestsView(authenticationHandler: authenticationHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PendingRequestsViewIdentifier: Codable, Hashable {}
|
|
||||||
@@ -13,7 +13,7 @@ import SwiftUI
|
|||||||
|
|
||||||
extension EnvironmentValues {
|
extension EnvironmentValues {
|
||||||
|
|
||||||
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
@MainActor fileprivate static let _certificateStore = CertificateStore()
|
||||||
|
|
||||||
@MainActor var certificateStore: CertificateStore {
|
@MainActor var certificateStore: CertificateStore {
|
||||||
EnvironmentValues._certificateStore
|
EnvironmentValues._certificateStore
|
||||||
@@ -49,20 +49,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
logger.debug("SecretAgent finished launching")
|
logger.debug("SecretAgent finished launching")
|
||||||
Task {
|
_ = Task {
|
||||||
for await session in socketController.sessions {
|
for await session in socketController.sessions {
|
||||||
Task {
|
_ = Task {
|
||||||
|
let inputParser = try await XPCAgentInputParser()
|
||||||
|
let hostsReader = try await XPCHostsfileReader()
|
||||||
|
let hosts = try? await hostsReader.read()
|
||||||
do {
|
do {
|
||||||
let inputParser = try await XPCAgentInputParser()
|
|
||||||
let hostsReader = try? await XPCHostsfileReader()
|
|
||||||
let hosts = try? await hostsReader?.read()
|
|
||||||
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, hosts: hosts)
|
let agentResponse = await agent.handle(request: request, provenance: session.provenance, hosts: hosts)
|
||||||
try session.write(agentResponse)
|
try session.write(agentResponse)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
try? session.close()
|
try session.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Foundation
|
|||||||
import Security
|
import Security
|
||||||
import CryptoTokenKit
|
import CryptoTokenKit
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import OSLog
|
import os
|
||||||
import SSHProtocolKit
|
import SSHProtocolKit
|
||||||
import CertificateKit
|
import CertificateKit
|
||||||
import SharedXPCServices
|
import SharedXPCServices
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import SecretKit
|
|||||||
import SecretAgentKit
|
import SecretAgentKit
|
||||||
import Brief
|
import Brief
|
||||||
|
|
||||||
typealias PersistAction = (@Sendable (AnySecret, TimeInterval) async throws -> Void)
|
|
||||||
|
|
||||||
final class Notifier: Sendable {
|
final class Notifier: Sendable {
|
||||||
|
|
||||||
private let notificationDelegate = NotificationDelegate()
|
private let notificationDelegate = NotificationDelegate()
|
||||||
@@ -17,12 +15,6 @@ final class Notifier: Sendable {
|
|||||||
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
|
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
|
||||||
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
|
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
|
||||||
|
|
||||||
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
|
|
||||||
UNUserNotificationCenter.current().delegate = notificationDelegate
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerPersistenceHandler(action: @escaping PersistAction) async {
|
|
||||||
let rawDurations = [
|
let rawDurations = [
|
||||||
Measurement(value: 1, unit: UnitDuration.minutes),
|
Measurement(value: 1, unit: UnitDuration.minutes),
|
||||||
Measurement(value: 5, unit: UnitDuration.minutes),
|
Measurement(value: 5, unit: UnitDuration.minutes),
|
||||||
@@ -32,9 +24,11 @@ final class Notifier: Sendable {
|
|||||||
|
|
||||||
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: .persistAuthenticationDeclineButton), options: [])
|
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: .persistAuthenticationDeclineButton), options: [])
|
||||||
var allPersistenceActions = [doNotPersistAction]
|
var allPersistenceActions = [doNotPersistAction]
|
||||||
|
|
||||||
let formatter = DateComponentsFormatter()
|
let formatter = DateComponentsFormatter()
|
||||||
formatter.unitsStyle = .spellOut
|
formatter.unitsStyle = .spellOut
|
||||||
formatter.allowedUnits = [.hour, .minute, .day]
|
formatter.allowedUnits = [.hour, .minute, .day]
|
||||||
|
|
||||||
var identifiers: [String: TimeInterval] = [:]
|
var identifiers: [String: TimeInterval] = [:]
|
||||||
for duration in rawDurations {
|
for duration in rawDurations {
|
||||||
let seconds = duration.converted(to: .seconds).value
|
let seconds = duration.converted(to: .seconds).value
|
||||||
@@ -49,11 +43,16 @@ final class Notifier: Sendable {
|
|||||||
if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
|
if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
|
||||||
persistAuthenticationCategory.setValue(String(localized: .persistAuthenticationAcceptButton), forKey: "_actionsMenuTitle")
|
persistAuthenticationCategory.setValue(String(localized: .persistAuthenticationAcceptButton), forKey: "_actionsMenuTitle")
|
||||||
}
|
}
|
||||||
var categories = await UNUserNotificationCenter.current().notificationCategories()
|
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
|
||||||
categories.insert(persistAuthenticationCategory)
|
UNUserNotificationCenter.current().delegate = notificationDelegate
|
||||||
UNUserNotificationCenter.current().setNotificationCategories(categories)
|
|
||||||
|
Task {
|
||||||
|
await notificationDelegate.state.setPersistenceState(options: identifiers) { secret, store, duration in
|
||||||
|
guard let duration = duration else { return }
|
||||||
|
try? await store.persistAuthentication(secret: secret, forDuration: duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await notificationDelegate.state.setPersistenceState(options: identifiers, action: action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prompt() {
|
func prompt() {
|
||||||
@@ -61,7 +60,7 @@ final class Notifier: Sendable {
|
|||||||
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
|
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?, offerPersistence: Bool) async {
|
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) async {
|
||||||
await notificationDelegate.state.setPending(secret: secret, store: store)
|
await notificationDelegate.state.setPending(secret: secret, store: store)
|
||||||
let notificationCenter = UNUserNotificationCenter.current()
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
let notificationContent = UNMutableNotificationContent()
|
let notificationContent = UNMutableNotificationContent()
|
||||||
@@ -77,7 +76,7 @@ final class Notifier: Sendable {
|
|||||||
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
|
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
|
||||||
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
|
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
|
||||||
notificationContent.interruptionLevel = .timeSensitive
|
notificationContent.interruptionLevel = .timeSensitive
|
||||||
if offerPersistence {
|
if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.authenticationRequirement.required {
|
||||||
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
|
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
|
||||||
}
|
}
|
||||||
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
||||||
@@ -116,8 +115,13 @@ extension Notifier: SigningWitness {
|
|||||||
) async throws {
|
) async throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?, offerPersistence: Bool) async throws {
|
func witness(
|
||||||
await notify(accessTo: secret, from: store, by: provenance, target: target, offerPersistence: offerPersistence)
|
accessTo secret: AnySecret,
|
||||||
|
from store: AnySecretStore,
|
||||||
|
by provenance: SigningRequestProvenance,
|
||||||
|
target: SigningRequestTarget?
|
||||||
|
) async throws {
|
||||||
|
await notify(accessTo: secret, from: store, by: provenance, target: target)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -146,24 +150,28 @@ extension Notifier {
|
|||||||
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
|
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
|
||||||
|
|
||||||
fileprivate actor State {
|
fileprivate actor State {
|
||||||
|
typealias PersistAction = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void)
|
||||||
typealias IgnoreAction = (@Sendable (Release) async -> Void)
|
typealias IgnoreAction = (@Sendable (Release) async -> Void)
|
||||||
fileprivate var release: Release?
|
fileprivate var release: Release?
|
||||||
fileprivate var ignoreAction: IgnoreAction?
|
fileprivate var ignoreAction: IgnoreAction?
|
||||||
fileprivate var persistAction: PersistAction?
|
fileprivate var persistAction: PersistAction?
|
||||||
fileprivate var persistOptions: [String: TimeInterval] = [:]
|
fileprivate var persistOptions: [String: TimeInterval] = [:]
|
||||||
|
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
|
||||||
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
|
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
|
||||||
|
|
||||||
func setPending(secret: AnySecret, store: AnySecretStore) {
|
func setPending(secret: AnySecret, store: AnySecretStore) {
|
||||||
pendingPersistableSecrets[secret.id.description] = secret
|
pendingPersistableSecrets[secret.id.description] = secret
|
||||||
|
pendingPersistableStores[store.id.description] = store
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrievePending(secretID: String, optionID: String) -> (AnySecret, TimeInterval)? {
|
func retrievePending(secretID: String, storeID: String, optionID: String) -> (AnySecret, AnySecretStore, TimeInterval)? {
|
||||||
guard let secret = pendingPersistableSecrets[secretID],
|
guard let secret = pendingPersistableSecrets[secretID],
|
||||||
|
let store = pendingPersistableStores[storeID],
|
||||||
let options = persistOptions[optionID] else {
|
let options = persistOptions[optionID] else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
pendingPersistableSecrets.removeValue(forKey: secretID)
|
pendingPersistableSecrets.removeValue(forKey: secretID)
|
||||||
return (secret, options)
|
return (secret, store, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) {
|
func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) {
|
||||||
@@ -211,12 +219,13 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
|
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
|
||||||
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String else {
|
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String,
|
||||||
|
let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let optionID = response.actionIdentifier
|
let optionID = response.actionIdentifier
|
||||||
guard let (secret, persistOptions) = await state.retrievePending(secretID: secretID, optionID: optionID) else { return }
|
guard let (secret, store, persistOptions) = await state.retrievePending(secretID: secretID, storeID: storeID, optionID: optionID) else { return }
|
||||||
try? await state.persistAction?(secret, persistOptions)
|
await state.persistAction?(secret, store, persistOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import SecretKit
|
|
||||||
import SecretAgentKit
|
|
||||||
import SmartCardSecretKit
|
|
||||||
import Common
|
|
||||||
|
|
||||||
struct PendingRequestsView: View {
|
|
||||||
|
|
||||||
private let authenticationHandler: any AuthenticationHandlerProtocol
|
|
||||||
@Environment(\.dismissWindow) var dismiss
|
|
||||||
|
|
||||||
init(authenticationHandler: some AuthenticationHandlerProtocol) {
|
|
||||||
self.authenticationHandler = authenticationHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
Text(.pendingRequestDescription)
|
|
||||||
ForEach(Array(authenticationHandler.batchableRequests.enumerated()), id: \.offset) { group in
|
|
||||||
MultilineInfoView {
|
|
||||||
if let first = group.element.first {
|
|
||||||
HStack {
|
|
||||||
HStack {
|
|
||||||
Image(nsImage: .init(byReferencing: first.provenance.origin.iconURL!))
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(width: 50)
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(first.provenance.origin.displayName)
|
|
||||||
.font(.subheadline)
|
|
||||||
Text(first.secret.name)
|
|
||||||
.font(.headline)
|
|
||||||
switch first.target {
|
|
||||||
case .connection(let payload):
|
|
||||||
if let host = payload.host {
|
|
||||||
Text(.authContextConnectingToUsernameAndHost(username: payload.username, host: host))
|
|
||||||
.font(.caption2)
|
|
||||||
} else {
|
|
||||||
Text(.authContextConnectingToUnknownHost)
|
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
case .signature(let payload):
|
|
||||||
Text(.authContextSigningForNamespace(namespace: payload.namespace))
|
|
||||||
.font(.caption2)
|
|
||||||
default:
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
Button(.pendingRequestsReviewBatchButton) {
|
|
||||||
Task {
|
|
||||||
try? await authenticationHandler.requestAuthentication(for: Set(group.element))
|
|
||||||
if authenticationHandler.batchableRequests.isEmpty {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonBorderShape(.capsule)
|
|
||||||
.primaryButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} items: {
|
|
||||||
ForEach(Array(group.element.enumerated()), id: \.offset) { pending in
|
|
||||||
HStack {
|
|
||||||
Text(pending.element.provenance.date.formatted())
|
|
||||||
Spacer()
|
|
||||||
Button(.pendingRequestsReviewSingleButton) {
|
|
||||||
Task {
|
|
||||||
try? await authenticationHandler.requestAuthentication(for: [pending.element])
|
|
||||||
if authenticationHandler.batchableRequests.isEmpty {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonBorderShape(.capsule)
|
|
||||||
.normalButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.safeAreaPadding(20)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct TestHandler: AuthenticationHandlerProtocol {
|
|
||||||
|
|
||||||
var batchableRequests: [[SignatureRequest]] = []
|
|
||||||
|
|
||||||
func requestAuthentication(for requests: Set<SignatureRequest>) async throws {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func persistAuthentication<SecretType>(secret: SecretType, forDuration duration: TimeInterval) async throws where SecretType : Secret {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func setPendingRequestHandler(_ handler: @escaping () async throws -> Void) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func authenticatedContext(for request: SignatureRequest, context: any AuthenticationContextProtocol) async throws -> (any AuthenticationContextProtocol)? {
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
if #available(macOS 26.0, *) {
|
|
||||||
ScrollView {
|
|
||||||
MultilineInfoView {
|
|
||||||
HStack {
|
|
||||||
HStack {
|
|
||||||
Image("ghostty")
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(width: 50)
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text("Ghostty")
|
|
||||||
.font(.subheadline)
|
|
||||||
Text("GitHub")
|
|
||||||
.font(.headline)
|
|
||||||
Text("Authenticating git@github.com")
|
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
Button("Review as Batch") {
|
|
||||||
|
|
||||||
}
|
|
||||||
.buttonBorderShape(.capsule)
|
|
||||||
.buttonStyle(.glassProminent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} items: {
|
|
||||||
ForEach(0..<2) { _ in
|
|
||||||
HStack {
|
|
||||||
Text("4:05 PM")
|
|
||||||
Spacer()
|
|
||||||
Button("Review") {
|
|
||||||
|
|
||||||
}
|
|
||||||
.buttonBorderShape(.capsule)
|
|
||||||
.buttonStyle(.glass)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
MultilineInfoView {
|
|
||||||
HStack {
|
|
||||||
HStack {
|
|
||||||
Image("ghostty")
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(width: 50)
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text("Ghostty")
|
|
||||||
.font(.subheadline)
|
|
||||||
Text("Git Signing")
|
|
||||||
.font(.headline)
|
|
||||||
Text("Git Signature")
|
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
Button("Review as Batch") {
|
|
||||||
|
|
||||||
}
|
|
||||||
.buttonBorderShape(.capsule)
|
|
||||||
.buttonStyle(.glassProminent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} items: {
|
|
||||||
ForEach(0..<2) { _ in
|
|
||||||
HStack {
|
|
||||||
Text("4:05 PM")
|
|
||||||
Spacer()
|
|
||||||
Button("Review") {
|
|
||||||
|
|
||||||
}
|
|
||||||
.buttonBorderShape(.capsule)
|
|
||||||
.buttonStyle(.glass)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.formStyle(.grouped)
|
|
||||||
.frame(minHeight: 700)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
<key>com.apple.security.hardened-process.dyld-ro</key>
|
<key>com.apple.security.hardened-process.dyld-ro</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
||||||
<string>2</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>
|
||||||
|
|||||||
@@ -6,10 +6,6 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.checked-allocations</key>
|
<key>com.apple.security.hardened-process.checked-allocations</key>
|
||||||
<true/>
|
<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>
|
<key>com.apple.security.hardened-process.dyld-ro</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ final class SecretAgentHostsfileReader: NSObject, XPCProtocol {
|
|||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.SecretAgentHostsfileReader", category: "SecretAgentHostsfileReader")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.SecretAgentHostsfileReader", category: "SecretAgentHostsfileReader")
|
||||||
|
|
||||||
func process(_ data: Data) async throws -> [Data: String] {
|
func process(_ data: Data) async throws -> [Data: String] {
|
||||||
logger.log("Parsing hostsfile")
|
logger.log("Parser parsed certificate")
|
||||||
var result: [Data: String] = [:]
|
var result: [Data: String] = [:]
|
||||||
for try await line in URL(filePath: NSHomeDirectory().appending("/.ssh/known_hosts")).lines {
|
// FIXME: THIS
|
||||||
|
for try await line in URL(filePath: "/Users/max/.ssh/known_hosts").lines {
|
||||||
let split = line.split(separator: " ").map(String.init)
|
let split = line.split(separator: " ").map(String.init)
|
||||||
guard split.count == 3 else { continue }
|
guard split.count == 3 else { continue }
|
||||||
guard let decoded = Data(base64Encoded: split[2]) else { continue }
|
guard let decoded = Data(base64Encoded: split[2]) else { continue }
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<key>com.apple.security.hardened-process.hardened-heap</key>
|
<key>com.apple.security.hardened-process.hardened-heap</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
||||||
<string>2</string>
|
<string>1</string>
|
||||||
<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>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,770 +0,0 @@
|
|||||||
{
|
|
||||||
"organization": "Max Goedjen",
|
|
||||||
"default-configuration": "Release",
|
|
||||||
"configurations": [
|
|
||||||
{ "name": "Debug", "file": "Config/Config.xcconfig" },
|
|
||||||
{ "name": "Release", "file": "Config/Config.xcconfig" },
|
|
||||||
{ "name": "Test", "file": "Config/Config.xcconfig" },
|
|
||||||
],
|
|
||||||
"localizations": {
|
|
||||||
"development": "en",
|
|
||||||
"supported": [
|
|
||||||
"ca",
|
|
||||||
"de",
|
|
||||||
"fi",
|
|
||||||
"fr",
|
|
||||||
"it",
|
|
||||||
"ko",
|
|
||||||
"pl",
|
|
||||||
"pt-BR",
|
|
||||||
"ru",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
{ "path": "Packages" },
|
|
||||||
{
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Secretive",
|
|
||||||
"children": [
|
|
||||||
{ "path": "App.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Views",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Configuration",
|
|
||||||
"children": [
|
|
||||||
{ "path": "ConfigurationItemView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "IntegrationsView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "Instructions.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "ToolConfigurationView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "SetupView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "GettingStartedView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Secrets",
|
|
||||||
"children": [
|
|
||||||
{ "path": "CreateSecretView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "DeleteSecretView.swift", "encoding": "utf8", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "EditSecretView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "DeleteCertificateView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "EditCertificateView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "EmptyStoreView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "NoStoresView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "SecretDetailView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "CertificateDetailView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "SecretListItemView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "CertificateListItemView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "StoreListView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "SettingsView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Modifiers",
|
|
||||||
"children": [
|
|
||||||
{ "path": "ErrorStyle.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "BoxBackgroundStyle.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "ToolbarButtonStyle.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Views",
|
|
||||||
"children": [
|
|
||||||
{ "path": "AboutView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "AgentStatusView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "ContentView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "CopyableView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "UpdateView.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Controllers",
|
|
||||||
"children": [
|
|
||||||
{ "path": "AgentStatusChecker.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "ApplicationDirectoryController.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "JustUpdatedChecker.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ "path": "Info.plist" },
|
|
||||||
{ "path": "InternetAccessPolicy.plist", "encoding": "utf8", "target-membership": [ "Secretive/resources" ] },
|
|
||||||
{
|
|
||||||
"path": "AppIcon.icon",
|
|
||||||
"target-membership": [
|
|
||||||
"Secretive/resources",
|
|
||||||
"SecretAgent/resources",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ "path": "Secretive.entitlements" },
|
|
||||||
{
|
|
||||||
"path": "<PROJECT>/Packages/Resources/Localizable.xcstrings",
|
|
||||||
"target-membership": [
|
|
||||||
"Secretive/resources",
|
|
||||||
"SecretAgent/resources",
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Preview Content",
|
|
||||||
"children": [
|
|
||||||
{ "path": "Preview Assets.xcassets", "target-membership": [ "Secretive/resources" ] },
|
|
||||||
{ "path": "PreviewStore.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "PreviewUpdater.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
{ "path": "PreviewAgentStatusChecker.swift", "target-membership": [ "Secretive/compile-sources" ] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "SecretAgent",
|
|
||||||
"children": [
|
|
||||||
{ "path": "App.swift", "encoding": "utf8", "target-membership": [ "SecretAgent/compile-sources" ] },
|
|
||||||
{ "path": "Notifier.swift", "target-membership": [ "SecretAgent/compile-sources" ] },
|
|
||||||
{ "path": "XPCInputParser.swift", "target-membership": [ "SecretAgent/compile-sources" ] },
|
|
||||||
{ "path": "PendingRequestsView.swift", "target-membership": [ "SecretAgent/compile-sources" ] },
|
|
||||||
{ "path": "XPCHostsfileReader.swift", "target-membership": [ "SecretAgent/compile-sources" ] },
|
|
||||||
{ "path": "CertificateMigrator.swift", "target-membership": [ "SecretAgent/compile-sources" ] },
|
|
||||||
{
|
|
||||||
"kind": "variant-group",
|
|
||||||
"name": "Main.storyboard",
|
|
||||||
"target-membership": [
|
|
||||||
"SecretAgent/resources",
|
|
||||||
],
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"path": "en.lproj/Main.storyboard",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ "path": "Info.plist" },
|
|
||||||
{ "path": "<PROJECT>/SecretAgent/InternetAccessPolicy.plist", "target-membership": [ "SecretAgent/resources" ] },
|
|
||||||
{ "path": "SecretAgent.entitlements" },
|
|
||||||
{
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Preview Content",
|
|
||||||
"children": [
|
|
||||||
{ "path": "Preview Assets.xcassets", "target-membership": [ "SecretAgent/resources" ] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "Config",
|
|
||||||
"children": [
|
|
||||||
{ "path": "Secretive.xctestplan" },
|
|
||||||
{ "path": "Config.xcconfig" },
|
|
||||||
{ "path": "OpenSource.xcconfig" },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "SecretiveUpdater",
|
|
||||||
"children": [
|
|
||||||
{ "path": "SecretiveUpdater.entitlements" },
|
|
||||||
{ "path": "Info.plist" },
|
|
||||||
{ "path": "InternetAccessPolicy.plist", "target-membership": [ "SecretiveUpdater/resources" ] },
|
|
||||||
{ "path": "main.swift", "target-membership": [ "SecretiveUpdater/compile-sources" ] },
|
|
||||||
{ "path": "SecretiveUpdater.swift", "target-membership": [ "SecretiveUpdater/compile-sources" ] },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "SecretAgentInputParser",
|
|
||||||
"children": [
|
|
||||||
{ "path": "SecretAgentInputParser.entitlements" },
|
|
||||||
{ "path": "Info.plist" },
|
|
||||||
{ "path": "main.swift", "target-membership": [ "SecretAgentInputParser/compile-sources" ] },
|
|
||||||
{ "path": "SecretAgentInputParser.swift", "target-membership": [ "SecretAgentInputParser/compile-sources" ] },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "SecretiveCertificateParser",
|
|
||||||
"children": [
|
|
||||||
{ "path": "Info.plist" },
|
|
||||||
{ "path": "main.swift", "target-membership": [ "SecretiveCertificateParser/compile-sources" ] },
|
|
||||||
{ "path": "SecretiveCertificateParser.entitlements" },
|
|
||||||
{ "path": "SecretiveCertificateParser.swift", "target-membership": [ "SecretiveCertificateParser/compile-sources" ] },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"path": "SecretAgentHostsfileReader",
|
|
||||||
"children": [
|
|
||||||
{ "path": "SecretAgentHostsfileReader.entitlements" },
|
|
||||||
{ "path": "Info.plist" },
|
|
||||||
{ "path": "main.swift", "target-membership": [ "SecretAgentHostsfileReader/compile-sources" ] },
|
|
||||||
{ "path": "SecretAgentHostsfileReader.swift", "target-membership": [ "SecretAgentHostsfileReader/compile-sources" ] },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"kind": "group",
|
|
||||||
"name": "Products",
|
|
||||||
"children": [
|
|
||||||
{ "path": "<PRODUCTS>/Secretive.app", "id": "50617D7F23FCE48E0099B055", "type": "wrapper.application", "index": false },
|
|
||||||
{ "path": "<PRODUCTS>/SecretAgent.app", "id": "50A3B78A24026B7500D209EA", "type": "wrapper.application", "index": false, "target-membership": [ { "build-phase": "Secretive/copy/Copy Files", "header-preservation": "remove-on-copy" } ] },
|
|
||||||
{
|
|
||||||
"path": "<PRODUCTS>/SecretiveUpdater.xpc",
|
|
||||||
"id": "50692D122E6FDB880043C7BB",
|
|
||||||
"type": "wrapper.xpc-service",
|
|
||||||
"index": false,
|
|
||||||
"target-membership": [
|
|
||||||
{ "build-phase": "Secretive/copy/Embed XPC Services", "header-preservation": "remove-on-copy" },
|
|
||||||
{ "build-phase": "SecretAgent/copy/Embed XPC Services", "header-preservation": "remove-on-copy" },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"path": "<PRODUCTS>/SecretAgentInputParser.xpc",
|
|
||||||
"id": "50692E502E6FF9D20043C7BB",
|
|
||||||
"type": "wrapper.xpc-service",
|
|
||||||
"index": false,
|
|
||||||
"target-membership": [
|
|
||||||
{ "build-phase": "Secretive/copy/Embed XPC Services", "header-preservation": "remove-on-copy" },
|
|
||||||
{ "build-phase": "SecretAgent/copy/Embed XPC Services", "header-preservation": "remove-on-copy" },
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
"path": "<PRODUCTS>/SecretiveCertificateParser.xpc",
|
|
||||||
"id": "50E205142FAAB81C00402380",
|
|
||||||
"type": "wrapper.xpc-service",
|
|
||||||
"index": false,
|
|
||||||
"target-membership": [
|
|
||||||
{ "build-phase": "Secretive/copy/Embed XPC Services", "header-preservation": "remove-on-copy" },
|
|
||||||
{ "build-phase": "SecretAgent/copy/Embed XPC Services", "header-preservation": "remove-on-copy" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ "path": "<PRODUCTS>/SecretAgentHostsfileReader.xpc", "id": "5054028D3034B5E3000C3356", "type": "wrapper.xpc-service", "index": false, "target-membership": [ { "build-phase": "SecretAgent/copy/Embed XPC Services", "header-preservation": "remove-on-copy" } ] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ "kind": "group", "name": "Frameworks" },
|
|
||||||
],
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"name": "Secretive",
|
|
||||||
"id": "50617D7E23FCE48D0099B055",
|
|
||||||
"product": "Products/Secretive.app",
|
|
||||||
"product-type": "application",
|
|
||||||
"dependencies": [
|
|
||||||
"SecretAgent",
|
|
||||||
"SecretiveUpdater",
|
|
||||||
"SecretAgentInputParser",
|
|
||||||
"SecretiveCertificateParser",
|
|
||||||
],
|
|
||||||
"build-phases": [
|
|
||||||
"compile-sources",
|
|
||||||
"frameworks",
|
|
||||||
"resources",
|
|
||||||
{
|
|
||||||
"kind": "copy",
|
|
||||||
"name": "Embed Frameworks",
|
|
||||||
"bundle-base-path": "frameworks-directory",
|
|
||||||
}, {
|
|
||||||
"kind": "copy",
|
|
||||||
"name": "Copy Files",
|
|
||||||
"bundle-base-path": "root",
|
|
||||||
"relative-path": "Contents/Library/LoginItems",
|
|
||||||
}, {
|
|
||||||
"kind": "copy",
|
|
||||||
"name": "Embed XPC Services",
|
|
||||||
"bundle-base-path": "build-products-directory",
|
|
||||||
"relative-path": "$(CONTENTS_FOLDER_PATH)/XPCServices",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"package-product-members": [
|
|
||||||
{
|
|
||||||
"product-name": "Brief",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "CertificateKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "Common",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "Formatters",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SecretKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SecureEnclaveSecretKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SettingsKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SharedXPCServices",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SmartCardSecretKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"build-settings": {
|
|
||||||
"ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon",
|
|
||||||
"CODE_SIGN_ENTITLEMENTS": "Secretive/Secretive.entitlements",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Debug]": "Apple Development",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Release]": "Developer ID Application",
|
|
||||||
"CODE_SIGN_STYLE[config=Debug]": "Automatic",
|
|
||||||
"CODE_SIGN_STYLE[config=Release]": "Manual",
|
|
||||||
"CODE_SIGN_STYLE[config=Test]": "Manual",
|
|
||||||
"COMBINE_HIDPI_IMAGES": "YES",
|
|
||||||
"CURRENT_PROJECT_VERSION": "1",
|
|
||||||
"DEAD_CODE_STRIPPING": "YES",
|
|
||||||
"DEVELOPMENT_ASSET_PATHS": "\"Secretive/Preview Content\"",
|
|
||||||
"DEVELOPMENT_TEAM[config=Debug]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"ENABLE_APP_SANDBOX": "YES",
|
|
||||||
"ENABLE_HARDENED_RUNTIME[config=Debug]": "YES",
|
|
||||||
"ENABLE_HARDENED_RUNTIME[config=Release]": "YES",
|
|
||||||
"ENABLE_HARDENED_RUNTIME[config=Test]": "NO",
|
|
||||||
"ENABLE_INCOMING_NETWORK_CONNECTIONS": "NO",
|
|
||||||
"ENABLE_OUTGOING_NETWORK_CONNECTIONS": "NO",
|
|
||||||
"ENABLE_PREVIEWS": "YES",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_AUDIO_INPUT": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_BLUETOOTH": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CALENDARS": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CAMERA": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CONTACTS": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_LOCATION": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_PRINTING": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_USB": "NO",
|
|
||||||
"INFOPLIST_FILE": "Secretive/Info.plist",
|
|
||||||
"LD_RUNPATH_SEARCH_PATHS": [
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/../Frameworks",
|
|
||||||
],
|
|
||||||
"MARKETING_VERSION": "1",
|
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER": "$(SECRETIVE_BASE_BUNDLE_ID).Host",
|
|
||||||
"PRODUCT_NAME": "$(TARGET_NAME)",
|
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[config=Debug]": "",
|
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[config=Release]": "Secretive - Host",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
"name": "SecretAgent",
|
|
||||||
"id": "50A3B78924026B7500D209EA",
|
|
||||||
"product": "Products/SecretAgent.app",
|
|
||||||
"product-type": "application",
|
|
||||||
"dependencies": [
|
|
||||||
"SecretiveUpdater",
|
|
||||||
"SecretAgentInputParser",
|
|
||||||
"SecretiveCertificateParser",
|
|
||||||
"SecretAgentHostsfileReader",
|
|
||||||
],
|
|
||||||
"build-phases": [
|
|
||||||
"compile-sources",
|
|
||||||
"frameworks",
|
|
||||||
"resources",
|
|
||||||
{
|
|
||||||
"kind": "copy",
|
|
||||||
"name": "Embed Frameworks",
|
|
||||||
"bundle-base-path": "frameworks-directory",
|
|
||||||
}, {
|
|
||||||
"kind": "copy",
|
|
||||||
"name": "Embed XPC Services",
|
|
||||||
"bundle-base-path": "build-products-directory",
|
|
||||||
"relative-path": "$(CONTENTS_FOLDER_PATH)/XPCServices",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"package-product-members": [
|
|
||||||
{
|
|
||||||
"product-name": "Brief",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "Common",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SecretAgentKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SecretKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SecureEnclaveSecretKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SharedXPCServices",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SmartCardSecretKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"build-settings": {
|
|
||||||
"ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon",
|
|
||||||
"AUTOMATION_APPLE_EVENTS": "NO",
|
|
||||||
"CODE_SIGN_ENTITLEMENTS": "SecretAgent/SecretAgent.entitlements",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Release]": "Developer ID Application",
|
|
||||||
"CODE_SIGN_STYLE[config=Debug]": "Automatic",
|
|
||||||
"CODE_SIGN_STYLE[config=Release]": "Manual",
|
|
||||||
"CODE_SIGN_STYLE[config=Test]": "Manual",
|
|
||||||
"COMBINE_HIDPI_IMAGES": "YES",
|
|
||||||
"DEAD_CODE_STRIPPING": "YES",
|
|
||||||
"DEVELOPMENT_ASSET_PATHS": "\"SecretAgent/Preview Content\"",
|
|
||||||
"DEVELOPMENT_TEAM[config=Debug]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"ENABLE_APP_SANDBOX": "YES",
|
|
||||||
"ENABLE_HARDENED_RUNTIME": "YES",
|
|
||||||
"ENABLE_INCOMING_NETWORK_CONNECTIONS": "NO",
|
|
||||||
"ENABLE_OUTGOING_NETWORK_CONNECTIONS": "NO",
|
|
||||||
"ENABLE_PREVIEWS": "YES",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_AUDIO_INPUT": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_BLUETOOTH": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CALENDARS": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CAMERA": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CONTACTS": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_LOCATION": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_PRINTING": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_USB": "NO",
|
|
||||||
"INFOPLIST_FILE": "SecretAgent/Info.plist",
|
|
||||||
"LD_RUNPATH_SEARCH_PATHS": [
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/../Frameworks",
|
|
||||||
],
|
|
||||||
"MARKETING_VERSION": "1",
|
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER": "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent",
|
|
||||||
"PRODUCT_NAME": "$(TARGET_NAME)",
|
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[config=Release]": "Secretive - Secret Agent",
|
|
||||||
"RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES": "NO",
|
|
||||||
"RUNTIME_EXCEPTION_ALLOW_JIT": "NO",
|
|
||||||
"RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY": "NO",
|
|
||||||
"RUNTIME_EXCEPTION_DEBUGGING_TOOL": "NO",
|
|
||||||
"RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION": "NO",
|
|
||||||
"RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION": "NO",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
"name": "SecretiveUpdater",
|
|
||||||
"id": "50692D112E6FDB880043C7BB",
|
|
||||||
"product": "Products/SecretiveUpdater.xpc",
|
|
||||||
"product-type": "xpc-service",
|
|
||||||
"dependencies": [
|
|
||||||
{ "kind": "package", "product-name": "Brief" },
|
|
||||||
{ "kind": "package", "product-name": "Formatters" },
|
|
||||||
{ "kind": "package", "product-name": "XPCWrappers" },
|
|
||||||
],
|
|
||||||
"build-phases": [
|
|
||||||
"compile-sources",
|
|
||||||
"frameworks",
|
|
||||||
"resources",
|
|
||||||
],
|
|
||||||
"package-product-members": [
|
|
||||||
{
|
|
||||||
"product-name": "Brief",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "Formatters",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "XPCWrappers",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"build-settings": {
|
|
||||||
"CLANG_CXX_LANGUAGE_STANDARD": "gnu++20",
|
|
||||||
"CODE_SIGN_ENTITLEMENTS": "SecretiveUpdater/SecretiveUpdater.entitlements",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Debug]": "Apple Development",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*]": "Apple Development",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Release]": "Developer ID Application",
|
|
||||||
"CODE_SIGN_STYLE[config=Debug]": "Automatic",
|
|
||||||
"CODE_SIGN_STYLE[config=Release]": "Manual",
|
|
||||||
"CODE_SIGN_STYLE[config=Test]": "Automatic",
|
|
||||||
"COMBINE_HIDPI_IMAGES": "YES",
|
|
||||||
"CURRENT_PROJECT_VERSION": "1",
|
|
||||||
"DEVELOPMENT_TEAM[config=Debug]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release]": "",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release][sdk=macosx*]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"ENABLE_APP_SANDBOX": "YES",
|
|
||||||
"ENABLE_HARDENED_RUNTIME": "YES",
|
|
||||||
"ENABLE_INCOMING_NETWORK_CONNECTIONS": "NO",
|
|
||||||
"ENABLE_OUTGOING_NETWORK_CONNECTIONS": "YES",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_AUDIO_INPUT": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_BLUETOOTH": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CALENDARS": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CAMERA": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_CONTACTS": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_LOCATION": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_PRINTING": "NO",
|
|
||||||
"ENABLE_RESOURCE_ACCESS_USB": "NO",
|
|
||||||
"GCC_C_LANGUAGE_STANDARD": "gnu17",
|
|
||||||
"GENERATE_INFOPLIST_FILE": "YES",
|
|
||||||
"INFOPLIST_FILE": "SecretiveUpdater/Info.plist",
|
|
||||||
"INFOPLIST_KEY_CFBundleDisplayName": "SecretiveUpdater",
|
|
||||||
"INFOPLIST_KEY_NSHumanReadableCopyright": "Copyright © 2025 Max Goedjen. All rights reserved.",
|
|
||||||
"LOCALIZATION_PREFERS_STRING_CATALOGS": "YES",
|
|
||||||
"MARKETING_VERSION": "1.0",
|
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER": "$(SECRETIVE_BASE_BUNDLE_ID).SecretiveUpdater",
|
|
||||||
"PRODUCT_NAME": "$(TARGET_NAME)",
|
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[config=Release]": "",
|
|
||||||
"REGISTER_APP_GROUPS": "YES",
|
|
||||||
"SKIP_INSTALL": "YES",
|
|
||||||
"STRING_CATALOG_GENERATE_SYMBOLS": "YES",
|
|
||||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG $(inherited)",
|
|
||||||
"SWIFT_APPROACHABLE_CONCURRENCY": "YES",
|
|
||||||
"SWIFT_EMIT_LOC_STRINGS": "YES",
|
|
||||||
"SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY": "YES",
|
|
||||||
"SWIFT_VERSION": "5.0",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
"name": "SecretiveCertificateParser",
|
|
||||||
"id": "50E205132FAAB81C00402380",
|
|
||||||
"product": "Products/SecretiveCertificateParser.xpc",
|
|
||||||
"product-type": "xpc-service",
|
|
||||||
"dependencies": [
|
|
||||||
{ "kind": "package", "product-name": "Formatters" },
|
|
||||||
{ "kind": "package", "product-name": "SSHProtocolKit" },
|
|
||||||
{ "kind": "package", "product-name": "XPCWrappers" },
|
|
||||||
],
|
|
||||||
"build-phases": [
|
|
||||||
"compile-sources",
|
|
||||||
"frameworks",
|
|
||||||
"resources",
|
|
||||||
],
|
|
||||||
"package-product-members": [
|
|
||||||
{
|
|
||||||
"product-name": "Formatters",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "SSHProtocolKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "XPCWrappers",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"build-settings": {
|
|
||||||
"CLANG_CXX_LANGUAGE_STANDARD": "gnu++20",
|
|
||||||
"CODE_SIGN_ENTITLEMENTS": "SecretiveCertificateParser/SecretiveCertificateParser.entitlements",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Debug]": "Apple Development",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Release]": "Developer ID Application",
|
|
||||||
"CODE_SIGN_STYLE[config=Debug]": "Automatic",
|
|
||||||
"CODE_SIGN_STYLE[config=Release]": "Manual",
|
|
||||||
"CODE_SIGN_STYLE[config=Test]": "Automatic",
|
|
||||||
"COMBINE_HIDPI_IMAGES": "YES",
|
|
||||||
"CURRENT_PROJECT_VERSION": "1",
|
|
||||||
"DEVELOPMENT_TEAM[config=Debug]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release]": "",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release][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",
|
|
||||||
"MARKETING_VERSION": "1.0",
|
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER": "$(SECRETIVE_BASE_BUNDLE_ID).SecretiveCertificateParser",
|
|
||||||
"PRODUCT_NAME": "$(TARGET_NAME)",
|
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[config=Release]": "",
|
|
||||||
"REGISTER_APP_GROUPS": "YES",
|
|
||||||
"SKIP_INSTALL": "YES",
|
|
||||||
"STRING_CATALOG_GENERATE_SYMBOLS": "YES",
|
|
||||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG $(inherited)",
|
|
||||||
"SWIFT_APPROACHABLE_CONCURRENCY": "YES",
|
|
||||||
"SWIFT_EMIT_LOC_STRINGS": "YES",
|
|
||||||
"SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY": "YES",
|
|
||||||
"SWIFT_VERSION": "5.0",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
"name": "SecretAgentInputParser",
|
|
||||||
"id": "50692E4F2E6FF9D20043C7BB",
|
|
||||||
"product": "Products/SecretAgentInputParser.xpc",
|
|
||||||
"product-type": "xpc-service",
|
|
||||||
"dependencies": [
|
|
||||||
{ "kind": "package", "product-name": "SecretAgentKit" },
|
|
||||||
{ "kind": "package", "product-name": "XPCWrappers" },
|
|
||||||
],
|
|
||||||
"build-phases": [
|
|
||||||
"compile-sources",
|
|
||||||
"frameworks",
|
|
||||||
"resources",
|
|
||||||
],
|
|
||||||
"package-product-members": [
|
|
||||||
{
|
|
||||||
"product-name": "SecretAgentKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "XPCWrappers",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"build-settings": {
|
|
||||||
"CLANG_CXX_LANGUAGE_STANDARD": "gnu++20",
|
|
||||||
"CODE_SIGN_ENTITLEMENTS": "SecretAgentInputParser/SecretAgentInputParser.entitlements",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Debug]": "Apple Development",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Release]": "Developer ID Application",
|
|
||||||
"CODE_SIGN_STYLE[config=Debug]": "Automatic",
|
|
||||||
"CODE_SIGN_STYLE[config=Release]": "Manual",
|
|
||||||
"CODE_SIGN_STYLE[config=Test]": "Automatic",
|
|
||||||
"COMBINE_HIDPI_IMAGES": "YES",
|
|
||||||
"CURRENT_PROJECT_VERSION": "1",
|
|
||||||
"DEVELOPMENT_TEAM[config=Debug]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release]": "",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release][sdk=macosx*]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"ENABLE_APP_SANDBOX": "YES",
|
|
||||||
"ENABLE_HARDENED_RUNTIME": "YES",
|
|
||||||
"GCC_C_LANGUAGE_STANDARD": "gnu17",
|
|
||||||
"GENERATE_INFOPLIST_FILE": "YES",
|
|
||||||
"INFOPLIST_FILE": "SecretAgentInputParser/Info.plist",
|
|
||||||
"INFOPLIST_KEY_CFBundleDisplayName": "SecretAgentInputParser",
|
|
||||||
"INFOPLIST_KEY_NSHumanReadableCopyright": "Copyright © 2025 Max Goedjen. All rights reserved.",
|
|
||||||
"LOCALIZATION_PREFERS_STRING_CATALOGS": "YES",
|
|
||||||
"MARKETING_VERSION": "1.0",
|
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER": "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgentInputParser",
|
|
||||||
"PRODUCT_NAME": "$(TARGET_NAME)",
|
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[config=Release]": "",
|
|
||||||
"REGISTER_APP_GROUPS": "YES",
|
|
||||||
"SKIP_INSTALL": "YES",
|
|
||||||
"STRING_CATALOG_GENERATE_SYMBOLS": "YES",
|
|
||||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG $(inherited)",
|
|
||||||
"SWIFT_APPROACHABLE_CONCURRENCY": "YES",
|
|
||||||
"SWIFT_EMIT_LOC_STRINGS": "YES",
|
|
||||||
"SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY": "YES",
|
|
||||||
"SWIFT_VERSION": "5.0",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
"name": "SecretAgentHostsfileReader",
|
|
||||||
"id": "5054028C3034B5E3000C3356",
|
|
||||||
"product": "Products/SecretAgentHostsfileReader.xpc",
|
|
||||||
"product-type": "xpc-service",
|
|
||||||
"dependencies": [
|
|
||||||
{ "kind": "package", "product-name": "SSHProtocolKit" },
|
|
||||||
{ "kind": "package", "product-name": "XPCWrappers" },
|
|
||||||
],
|
|
||||||
"build-phases": [
|
|
||||||
"compile-sources",
|
|
||||||
"frameworks",
|
|
||||||
"resources",
|
|
||||||
],
|
|
||||||
"package-product-members": [
|
|
||||||
{
|
|
||||||
"product-name": "SSHProtocolKit",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
}, {
|
|
||||||
"product-name": "XPCWrappers",
|
|
||||||
"build-phase": { "build-phase": "frameworks" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"build-settings": {
|
|
||||||
"CLANG_CXX_LANGUAGE_STANDARD": "gnu++20",
|
|
||||||
"CODE_SIGN_ENTITLEMENTS": "SecretAgentHostsfileReader/SecretAgentHostsfileReader.entitlements",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Debug]": "Apple Development",
|
|
||||||
"CODE_SIGN_IDENTITY[config=Release]": "Developer ID Application",
|
|
||||||
"CODE_SIGN_STYLE[config=Debug]": "Automatic",
|
|
||||||
"CODE_SIGN_STYLE[config=Release]": "Manual",
|
|
||||||
"CODE_SIGN_STYLE[config=Test]": "Automatic",
|
|
||||||
"COMBINE_HIDPI_IMAGES": "YES",
|
|
||||||
"CURRENT_PROJECT_VERSION": "1",
|
|
||||||
"DEVELOPMENT_TEAM[config=Debug]": "$(SECRETIVE_DEVELOPMENT_TEAM)",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release]": "",
|
|
||||||
"DEVELOPMENT_TEAM[config=Release][sdk=macosx*]": "Z72PRUAWF6",
|
|
||||||
"ENABLE_APP_SANDBOX": "NO",
|
|
||||||
"ENABLE_HARDENED_RUNTIME": "YES",
|
|
||||||
"GCC_C_LANGUAGE_STANDARD": "gnu17",
|
|
||||||
"GENERATE_INFOPLIST_FILE": "YES",
|
|
||||||
"INFOPLIST_FILE": "SecretAgentHostsfileReader/Info.plist",
|
|
||||||
"INFOPLIST_KEY_CFBundleDisplayName": "SecretAgentHostsfileReader",
|
|
||||||
"INFOPLIST_KEY_NSHumanReadableCopyright": "Copyright © 2026 Max Goedjen. All rights reserved.",
|
|
||||||
"LOCALIZATION_PREFERS_STRING_CATALOGS": "YES",
|
|
||||||
"MARKETING_VERSION": "1.0",
|
|
||||||
"PRODUCT_BUNDLE_IDENTIFIER": "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgentHostsfileReader",
|
|
||||||
"PRODUCT_NAME": "$(TARGET_NAME)",
|
|
||||||
"PROVISIONING_PROFILE_SPECIFIER[config=Release]": "",
|
|
||||||
"REGISTER_APP_GROUPS": "YES",
|
|
||||||
"SKIP_INSTALL": "YES",
|
|
||||||
"STRING_CATALOG_GENERATE_SYMBOLS": "YES",
|
|
||||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG $(inherited)",
|
|
||||||
"SWIFT_APPROACHABLE_CONCURRENCY": "YES",
|
|
||||||
"SWIFT_EMIT_LOC_STRINGS": "YES",
|
|
||||||
"SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY": "YES",
|
|
||||||
"SWIFT_VERSION": "5.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"build-settings": {
|
|
||||||
"ALWAYS_SEARCH_USER_PATHS": "NO",
|
|
||||||
"ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS": "YES",
|
|
||||||
"CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED": "YES",
|
|
||||||
"CLANG_ANALYZER_NONNULL": "YES",
|
|
||||||
"CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION": "YES_AGGRESSIVE",
|
|
||||||
"CLANG_CXX_LANGUAGE_STANDARD": "gnu++14",
|
|
||||||
"CLANG_CXX_LIBRARY": "libc++",
|
|
||||||
"CLANG_ENABLE_MODULES": "YES",
|
|
||||||
"CLANG_ENABLE_OBJC_ARC": "YES",
|
|
||||||
"CLANG_ENABLE_OBJC_WEAK": "YES",
|
|
||||||
"CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING": "YES",
|
|
||||||
"CLANG_WARN_BOOL_CONVERSION": "YES",
|
|
||||||
"CLANG_WARN_COMMA": "YES",
|
|
||||||
"CLANG_WARN_CONSTANT_CONVERSION": "YES",
|
|
||||||
"CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS": "YES",
|
|
||||||
"CLANG_WARN_DIRECT_OBJC_ISA_USAGE": "YES_ERROR",
|
|
||||||
"CLANG_WARN_DOCUMENTATION_COMMENTS": "YES",
|
|
||||||
"CLANG_WARN_EMPTY_BODY": "YES",
|
|
||||||
"CLANG_WARN_ENUM_CONVERSION": "YES",
|
|
||||||
"CLANG_WARN_INFINITE_RECURSION": "YES",
|
|
||||||
"CLANG_WARN_INT_CONVERSION": "YES",
|
|
||||||
"CLANG_WARN_NON_LITERAL_NULL_CONVERSION": "YES",
|
|
||||||
"CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF": "YES",
|
|
||||||
"CLANG_WARN_OBJC_LITERAL_CONVERSION": "YES",
|
|
||||||
"CLANG_WARN_OBJC_ROOT_CLASS": "YES_ERROR",
|
|
||||||
"CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER": "YES",
|
|
||||||
"CLANG_WARN_RANGE_LOOP_ANALYSIS": "YES",
|
|
||||||
"CLANG_WARN_STRICT_PROTOTYPES": "YES",
|
|
||||||
"CLANG_WARN_SUSPICIOUS_MOVE": "YES",
|
|
||||||
"CLANG_WARN_UNGUARDED_AVAILABILITY": "YES_AGGRESSIVE",
|
|
||||||
"CLANG_WARN_UNREACHABLE_CODE": "YES",
|
|
||||||
"CLANG_WARN__DUPLICATE_METHOD_MATCH": "YES",
|
|
||||||
"COPY_PHASE_STRIP": "NO",
|
|
||||||
"DEAD_CODE_STRIPPING": "YES",
|
|
||||||
"DEBUG_INFORMATION_FORMAT[config=Debug]": "dwarf",
|
|
||||||
"DEBUG_INFORMATION_FORMAT[config=Release]": "dwarf-with-dsym",
|
|
||||||
"DEBUG_INFORMATION_FORMAT[config=Test]": "dwarf",
|
|
||||||
"ENABLE_ENHANCED_SECURITY": "YES",
|
|
||||||
"ENABLE_NS_ASSERTIONS[config=Release]": "NO",
|
|
||||||
"ENABLE_POINTER_AUTHENTICATION": "YES",
|
|
||||||
"ENABLE_STRICT_OBJC_MSGSEND": "YES",
|
|
||||||
"ENABLE_TESTABILITY[config=Debug]": "YES",
|
|
||||||
"ENABLE_TESTABILITY[config=Test]": "YES",
|
|
||||||
"ENABLE_USER_SCRIPT_SANDBOXING": "YES",
|
|
||||||
"GCC_C_LANGUAGE_STANDARD": "gnu11",
|
|
||||||
"GCC_DYNAMIC_NO_PIC[config=Debug]": "NO",
|
|
||||||
"GCC_DYNAMIC_NO_PIC[config=Test]": "NO",
|
|
||||||
"GCC_NO_COMMON_BLOCKS": "YES",
|
|
||||||
"GCC_OPTIMIZATION_LEVEL[config=Debug]": "0",
|
|
||||||
"GCC_OPTIMIZATION_LEVEL[config=Test]": "0",
|
|
||||||
"GCC_PREPROCESSOR_DEFINITIONS[config=Debug]": [
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
],
|
|
||||||
"GCC_PREPROCESSOR_DEFINITIONS[config=Test]": [
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
],
|
|
||||||
"GCC_WARN_64_TO_32_BIT_CONVERSION": "YES",
|
|
||||||
"GCC_WARN_ABOUT_RETURN_TYPE": "YES_ERROR",
|
|
||||||
"GCC_WARN_UNDECLARED_SELECTOR": "YES",
|
|
||||||
"GCC_WARN_UNINITIALIZED_AUTOS": "YES_AGGRESSIVE",
|
|
||||||
"GCC_WARN_UNUSED_FUNCTION": "YES",
|
|
||||||
"GCC_WARN_UNUSED_VARIABLE": "YES",
|
|
||||||
"MACOSX_DEPLOYMENT_TARGET": "15.0",
|
|
||||||
"MTL_ENABLE_DEBUG_INFO[config=Debug]": "INCLUDE_SOURCE",
|
|
||||||
"MTL_ENABLE_DEBUG_INFO[config=Release]": "NO",
|
|
||||||
"MTL_ENABLE_DEBUG_INFO[config=Test]": "INCLUDE_SOURCE",
|
|
||||||
"MTL_FAST_MATH": "YES",
|
|
||||||
"ONLY_ACTIVE_ARCH[config=Debug]": "YES",
|
|
||||||
"ONLY_ACTIVE_ARCH[config=Test]": "YES",
|
|
||||||
"OTHER_SWIFT_FLAGS": "",
|
|
||||||
"SDKROOT": "macosx",
|
|
||||||
"STRING_CATALOG_GENERATE_SYMBOLS": "YES",
|
|
||||||
"STRIP_INSTALLED_PRODUCT": "NO",
|
|
||||||
"STRIP_SWIFT_SYMBOLS": "NO",
|
|
||||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug]": "DEBUG",
|
|
||||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Test]": "DEBUG",
|
|
||||||
"SWIFT_COMPILATION_MODE[config=Release]": "wholemodule",
|
|
||||||
"SWIFT_EMIT_LOC_STRINGS": "YES",
|
|
||||||
"SWIFT_OPTIMIZATION_LEVEL[config=Debug]": "-Onone",
|
|
||||||
"SWIFT_OPTIMIZATION_LEVEL[config=Release]": "-O",
|
|
||||||
"SWIFT_OPTIMIZATION_LEVEL[config=Test]": "-Onone",
|
|
||||||
"SWIFT_STRICT_MEMORY_SAFETY": "YES",
|
|
||||||
"SWIFT_TREAT_WARNINGS_AS_ERRORS": "YES",
|
|
||||||
"SWIFT_VERSION": "6.0",
|
|
||||||
},
|
|
||||||
"last-upgrade": "27.0",
|
|
||||||
"last-swift-update": "27.0",
|
|
||||||
}
|
|
||||||
+4
-6
@@ -1,10 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>iOSPackagesShouldBuildARM64e</key>
|
<key>iOSPackagesShouldBuildARM64e</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>macOSPackagesShouldBuildARM64e</key>
|
</dict>
|
||||||
<true />
|
|
||||||
</dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
@@ -4,7 +4,6 @@ import SecureEnclaveSecretKit
|
|||||||
import SmartCardSecretKit
|
import SmartCardSecretKit
|
||||||
import Brief
|
import Brief
|
||||||
import CertificateKit
|
import CertificateKit
|
||||||
import SettingsKit
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Secretive: App {
|
struct Secretive: App {
|
||||||
@@ -17,7 +16,6 @@ struct Secretive: App {
|
|||||||
ContentView()
|
ContentView()
|
||||||
.environment(EnvironmentValues._secretStoreList)
|
.environment(EnvironmentValues._secretStoreList)
|
||||||
.environment(EnvironmentValues._certificateStore)
|
.environment(EnvironmentValues._certificateStore)
|
||||||
.environment(EnvironmentValues._settingsStore)
|
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||||
Task {
|
Task {
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||||
@@ -44,9 +42,6 @@ struct Secretive: App {
|
|||||||
}
|
}
|
||||||
.windowStyle(.hiddenTitleBar)
|
.windowStyle(.hiddenTitleBar)
|
||||||
.windowResizability(.contentSize)
|
.windowResizability(.contentSize)
|
||||||
Settings {
|
|
||||||
SettingsView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -106,8 +101,7 @@ extension EnvironmentValues {
|
|||||||
return list
|
return list
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@MainActor fileprivate static let _certificateStore = CertificateStore()
|
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
||||||
@MainActor fileprivate static let _settingsStore = SettingsStore()
|
|
||||||
|
|
||||||
private static let _agentLaunchController = AgentLaunchController()
|
private static let _agentLaunchController = AgentLaunchController()
|
||||||
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
||||||
@@ -128,10 +122,6 @@ extension EnvironmentValues {
|
|||||||
@MainActor var certificateStore: CertificateStore {
|
@MainActor var certificateStore: CertificateStore {
|
||||||
EnvironmentValues._certificateStore
|
EnvironmentValues._certificateStore
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor var settingsStore: SettingsStore {
|
|
||||||
EnvironmentValues._settingsStore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FocusedValues {
|
extension FocusedValues {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import LocalAuthentication
|
|
||||||
|
|
||||||
enum Preview {}
|
enum Preview {}
|
||||||
|
|
||||||
@@ -15,6 +14,7 @@ extension Preview {
|
|||||||
Attributes(
|
Attributes(
|
||||||
keyType: .init(algorithm: .ecdsa, size: 256),
|
keyType: .init(algorithm: .ecdsa, size: 256),
|
||||||
authentication: .presenceRequired,
|
authentication: .presenceRequired,
|
||||||
|
restrictions: .default
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,10 +39,17 @@ extension Preview {
|
|||||||
self.init(secrets: new)
|
self.init(secrets: new)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?, context: LAContext?) async throws -> Data {
|
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws -> Data {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
|
||||||
|
}
|
||||||
|
|
||||||
func reloadSecrets() {
|
func reloadSecrets() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,10 +83,16 @@ extension Preview {
|
|||||||
self.init(secrets: new)
|
self.init(secrets: new)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?, context: LAContext?) async throws -> Data {
|
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws -> Data {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
|
||||||
|
}
|
||||||
|
|
||||||
func reloadSecrets() {
|
func reloadSecrets() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<key>com.apple.security.hardened-process.hardened-heap</key>
|
<key>com.apple.security.hardened-process.hardened-heap</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
||||||
<string>2</string>
|
<string>1</string>
|
||||||
<key>com.apple.security.smartcard</key>
|
<key>com.apple.security.smartcard</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
|
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ struct IntegrationsView: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toolbarBackgroundVisibility(.hidden, for: .automatic)
|
.hiddenToolbar()
|
||||||
.containerBackground(.thinMaterial, for: .window)
|
.windowBackgroundStyle(.thinMaterial)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
selectedInstruction = instructions.gettingStarted
|
selectedInstruction = instructions.gettingStarted
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -1,11 +1,11 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct PrimaryButtonModifier: ViewModifier {
|
struct PrimaryButtonModifier: ViewModifier {
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@Environment(\.isEnabled) var isEnabled
|
@Environment(\.isEnabled) var isEnabled
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
// Tinted glass prominent is really hard to read on 26.0.
|
// Tinted glass prominent is really hard to read on 26.0.
|
||||||
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
|
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
|
||||||
content.buttonStyle(.glassProminent)
|
content.buttonStyle(.glassProminent)
|
||||||
@@ -18,15 +18,15 @@ public struct PrimaryButtonModifier: ViewModifier {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
public func primaryButton() -> some View {
|
func primaryButton() -> some View {
|
||||||
modifier(PrimaryButtonModifier())
|
modifier(PrimaryButtonModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ToolbarCircleButtonModifier: ViewModifier {
|
struct ToolbarCircleButtonModifier: ViewModifier {
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
content
|
content
|
||||||
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
|
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
|
||||||
@@ -40,15 +40,15 @@ public struct ToolbarCircleButtonModifier: ViewModifier {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
public func toolbarCircleButton() -> some View {
|
func toolbarCircleButton() -> some View {
|
||||||
modifier(ToolbarCircleButtonModifier())
|
modifier(ToolbarCircleButtonModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct NormalButtonModifier: ViewModifier {
|
struct NormalButtonModifier: ViewModifier {
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
content.buttonStyle(.glass)
|
content.buttonStyle(.glass)
|
||||||
} else {
|
} else {
|
||||||
@@ -60,17 +60,17 @@ public struct NormalButtonModifier: ViewModifier {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
public func normalButton() -> some View {
|
func normalButton() -> some View {
|
||||||
modifier(NormalButtonModifier())
|
modifier(NormalButtonModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct DangerButtonModifier: ViewModifier {
|
struct DangerButtonModifier: ViewModifier {
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
// Tinted glass prominent is really hard to read on 26.0.
|
// Tinted glass prominent is really hard to read on 26.0.
|
||||||
if #available(macOS 26.0, *), colorScheme == .dark {
|
if #available(macOS 26.0, *), colorScheme == .dark {
|
||||||
content.buttonStyle(.glassProminent)
|
content.buttonStyle(.glassProminent)
|
||||||
@@ -87,7 +87,7 @@ public struct DangerButtonModifier: ViewModifier {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
public func danger() -> some View {
|
func danger() -> some View {
|
||||||
modifier(DangerButtonModifier())
|
modifier(DangerButtonModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WindowBackgroundStyleModifier: ViewModifier {
|
||||||
|
|
||||||
|
let shapeStyle: any ShapeStyle
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
if #available(macOS 15.0, *) {
|
||||||
|
content
|
||||||
|
.containerBackground(
|
||||||
|
shapeStyle, for: .window
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
func windowBackgroundStyle(_ style: some ShapeStyle) -> some View {
|
||||||
|
modifier(WindowBackgroundStyleModifier(shapeStyle: style))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HiddenToolbarModifier: ViewModifier {
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
if #available(macOS 15.0, *) {
|
||||||
|
content
|
||||||
|
.toolbarBackgroundVisibility(.hidden, for: .automatic)
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
func hiddenToolbar() -> some View {
|
||||||
|
modifier(HiddenToolbarModifier())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,8 +10,9 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
@State private var name = ""
|
@State private var name = ""
|
||||||
@State private var keyAttribution = ""
|
@State private var keyAttribution = ""
|
||||||
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
|
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
|
||||||
|
@State private var restrictions: Restrictions = .default
|
||||||
@State private var keyType: KeyType?
|
@State private var keyType: KeyType?
|
||||||
@State var advanced = false
|
@State var advanced = true // FIXME: Set back
|
||||||
@State var errorText: String?
|
@State var errorText: String?
|
||||||
|
|
||||||
private var authenticationOptions: [AuthenticationRequirement] {
|
private var authenticationOptions: [AuthenticationRequirement] {
|
||||||
@@ -72,6 +73,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if advanced {
|
if advanced {
|
||||||
|
SecretRestrictionsView(restrictions: $restrictions)
|
||||||
Section {
|
Section {
|
||||||
VStack {
|
VStack {
|
||||||
Picker(.createSecretKeyTypeLabel, selection: $keyType) {
|
Picker(.createSecretKeyTypeLabel, selection: $keyType) {
|
||||||
@@ -107,6 +109,8 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Key Properties")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let errorText {
|
if let errorText {
|
||||||
@@ -146,6 +150,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
attributes: .init(
|
attributes: .init(
|
||||||
keyType: keyType!,
|
keyType: keyType!,
|
||||||
authentication: authenticationRequirement,
|
authentication: authenticationRequirement,
|
||||||
|
restrictions: restrictions,
|
||||||
publicKeyAttribution: attribution
|
publicKeyAttribution: attribution
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -158,7 +163,52 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
struct SecretRestrictionsView: View {
|
||||||
|
|
||||||
//#Preview {
|
@Binding var restrictions: Restrictions
|
||||||
// CreateSecretView(store: Preview.StoreModifiable()) { _ in }
|
|
||||||
//}
|
struct IdentifiedString: Identifiable {
|
||||||
|
let value: String
|
||||||
|
var id: String { value }
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
Toggle("Allow Forwarding", isOn: $restrictions.allowForwarding)
|
||||||
|
Toggle("Allow Signing Operations", isOn: $restrictions.allowSigning)
|
||||||
|
Toggle("Allow Connection Operations", isOn: $restrictions.allowConnections)
|
||||||
|
Picker(selection: $restrictions.allowedDomains) {
|
||||||
|
Text("All")
|
||||||
|
.tag(Restrictions.AllowedDomains.all)
|
||||||
|
Text("Specific")
|
||||||
|
.tag(Restrictions.AllowedDomains.specific([]))
|
||||||
|
} label: {
|
||||||
|
Text("Allowed Domains")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Restrictions")
|
||||||
|
}
|
||||||
|
if restrictions.allowedDomains != .all {
|
||||||
|
Section {
|
||||||
|
switch restrictions.allowedDomains {
|
||||||
|
case .all:
|
||||||
|
EmptyView()
|
||||||
|
case .specific(let array):
|
||||||
|
if restrictions.allowedDomains != .all {
|
||||||
|
ForEach((array + [""]).map({IdentifiedString(value: $0)})) {
|
||||||
|
TextField("", text: .constant($0.value), prompt: Text("example.com"))
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Allowed Domains")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
CreateSecretView(store: Preview.StoreModifiable()) { _ in }
|
||||||
|
.frame(height: 1000)
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,13 +28,6 @@ struct SecretDetailView<SecretType: Secret>: View {
|
|||||||
image: Image(systemName: "touchid"),
|
image: Image(systemName: "touchid"),
|
||||||
text: keyWriter.openSSHMD5Fingerprint(secret: secret)
|
text: keyWriter.openSSHMD5Fingerprint(secret: secret)
|
||||||
)
|
)
|
||||||
Spacer()
|
|
||||||
.frame(height: 20)
|
|
||||||
CopyableView(
|
|
||||||
title: .secretDetailPublicKeyLabel,
|
|
||||||
image: Image(systemName: "key"),
|
|
||||||
text: keyWriter.openSSHString(secret: secret)
|
|
||||||
)
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 20)
|
.frame(height: 20)
|
||||||
CopyableView(
|
CopyableView(
|
||||||
@@ -46,14 +39,18 @@ struct SecretDetailView<SecretType: Secret>: View {
|
|||||||
if !certificates.isEmpty {
|
if !certificates.isEmpty {
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 20)
|
.frame(height: 20)
|
||||||
MultilineInfoView(title: .secretDetailCertificatePathLabel, image: Image(systemName: "checkmark.seal.text.page")) {
|
MultilineInfoView(
|
||||||
ForEach(certificates) { certificate in
|
title: .secretDetailCertificatePathLabel,
|
||||||
Text(certificate.name)
|
image: Image(
|
||||||
.multilineItemAction(image: Image(systemName: "chevron.forward")) {
|
systemName: "checkmark.seal.text.page"
|
||||||
navigateToCertificate?(certificate)
|
),
|
||||||
}
|
items: certificates.map({ certificate in
|
||||||
}
|
MultilineInfoView.Item(
|
||||||
}
|
text: certificate.name,
|
||||||
|
action: (Image(systemName: "chevron.forward"), { navigateToCertificate?(certificate) })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import SettingsKit
|
|
||||||
|
|
||||||
struct SettingsView: View {
|
|
||||||
|
|
||||||
@Environment(\.settingsStore) var settingsStore
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.frame(minWidth: 480, minHeight: 320)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,7 +44,7 @@ struct StoreListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !certificateStore.certificates.isEmpty {
|
if !certificateStore.certificates.isEmpty {
|
||||||
Section(.certificatesSectionTitle) {
|
Section("Certificates") {
|
||||||
ForEach(certificateStore.certificates) { certificate in
|
ForEach(certificateStore.certificates) { certificate in
|
||||||
CertificateListItemView(
|
CertificateListItemView(
|
||||||
certificate: certificate,
|
certificate: certificate,
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import SwiftUI
|
|||||||
|
|
||||||
struct AboutView: View {
|
struct AboutView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
AboutViewContent()
|
if #available(macOS 15.0, *) {
|
||||||
.containerBackground(
|
AboutViewContent()
|
||||||
.thinMaterial, for: .window
|
.containerBackground(
|
||||||
)
|
.thinMaterial, for: .window
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AboutViewContent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
switch interactionState {
|
switch interactionState {
|
||||||
case .normal:
|
case .normal:
|
||||||
return base.mix(with: .accentColor, by: 0)
|
return base
|
||||||
case .hovering:
|
case .hovering:
|
||||||
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
|
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
|
||||||
case .clicking, .dragging:
|
case .clicking, .dragging:
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
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)
|
||||||
|
.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
|
||||||
|
.contentShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
|
.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: Color
|
||||||
|
if #available(macOS 27.0, *) {
|
||||||
|
base = .clear
|
||||||
|
} else {
|
||||||
|
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()
|
||||||
|
}
|
||||||
@@ -12,11 +12,11 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.dyld-ro</key>
|
<key>com.apple.security.hardened-process.dyld-ro</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
<key>com.apple.security.hardened-process.enhanced-security-version</key>
|
||||||
<string>2</string>
|
<integer>1</integer>
|
||||||
<key>com.apple.security.hardened-process.hardened-heap</key>
|
<key>com.apple.security.hardened-process.hardened-heap</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
|
<key>com.apple.security.hardened-process.platform-restrictions</key>
|
||||||
<string>2</string>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>com.apple.security.hardened-process.dyld-ro</key>
|
<key>com.apple.security.hardened-process.dyld-ro</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
<key>com.apple.security.hardened-process.enhanced-security-version-string</key>
|
||||||
<string>2</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.hardened-process.platform-restrictions-string</key>
|
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
|
||||||
|
|||||||
Reference in New Issue
Block a user