mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-06-25 15:41:34 +00:00
Compare commits
7 Commits
auth_split
...
socket_lau
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
688b6380bd | ||
|
|
702e3f2cb0 | ||
|
|
76baba746c | ||
|
|
18107257ba | ||
|
|
98e2f38e46 | ||
|
|
a727d110c8 | ||
|
|
fbc4133f39 |
5
.github/workflows/nightly.yml
vendored
5
.github/workflows/nightly.yml
vendored
@@ -11,6 +11,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
attestations: write
|
attestations: write
|
||||||
|
artifact-metadata: write
|
||||||
actions: read
|
actions: read
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
@@ -40,7 +41,7 @@ jobs:
|
|||||||
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
||||||
- name: Upload App to Artifacts
|
- name: Upload App to Artifacts
|
||||||
id: upload
|
id: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Secretive
|
name: Secretive
|
||||||
path: Artifact
|
path: Artifact
|
||||||
@@ -59,7 +60,7 @@ jobs:
|
|||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
- name: Attest
|
- name: Attest
|
||||||
id: attest
|
id: attest
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4
|
||||||
with:
|
with:
|
||||||
subject-name: "Secretive.zip"
|
subject-name: "Secretive.zip"
|
||||||
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
||||||
|
|||||||
5
.github/workflows/oneoff.yml
vendored
5
.github/workflows/oneoff.yml
vendored
@@ -10,6 +10,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
attestations: write
|
attestations: write
|
||||||
|
artifact-metadata: write
|
||||||
actions: read
|
actions: read
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
@@ -39,7 +40,7 @@ jobs:
|
|||||||
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
||||||
- name: Upload App to Artifacts
|
- name: Upload App to Artifacts
|
||||||
id: upload
|
id: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Secretive
|
name: Secretive
|
||||||
path: Artifact
|
path: Artifact
|
||||||
@@ -58,7 +59,7 @@ jobs:
|
|||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
- name: Attest
|
- name: Attest
|
||||||
id: attest
|
id: attest
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4
|
||||||
with:
|
with:
|
||||||
subject-name: "Secretive.zip"
|
subject-name: "Secretive.zip"
|
||||||
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
||||||
|
|||||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -32,6 +32,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
attestations: write
|
attestations: write
|
||||||
|
artifact-metadata: write
|
||||||
actions: read
|
actions: read
|
||||||
runs-on: macos-26
|
runs-on: macos-26
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
@@ -63,7 +64,7 @@ jobs:
|
|||||||
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
|
||||||
- name: Upload App to Artifacts
|
- name: Upload App to Artifacts
|
||||||
id: upload
|
id: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Secretive.zip
|
name: Secretive.zip
|
||||||
path: Artifact
|
path: Artifact
|
||||||
@@ -82,7 +83,7 @@ jobs:
|
|||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
- name: Attest
|
- name: Attest
|
||||||
id: attest
|
id: attest
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4
|
||||||
with:
|
with:
|
||||||
subject-path: "Secretive.zip"
|
subject-path: "Secretive.zip"
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
|
|||||||
@@ -365,16 +365,6 @@
|
|||||||
},
|
},
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
"%@ - %@" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "%1$@ - %2$@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"about_build_log_button" : {
|
"about_build_log_button" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1855,191 +1845,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent_details_running_since_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"af" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ar" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ca" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Funcionant des de"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cs" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"da" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Läuft seit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"el" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Εκτελείται Από"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fi" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Käynnissä alkaen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Actif depuis"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"he" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hu" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"it" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "最後に起動した日時"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "실행 시각"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nb" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nl" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pl" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Uruchomiony od"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pt" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pt-BR" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Rodando desde"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ro" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ru" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Запущено с"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sv" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uk" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vi" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Running Since"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"zh-Hans" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "运行时间始于"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"zh-Hant" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "執行時間始於"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agent_details_socket_path_title" : {
|
"agent_details_socket_path_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2780,7 +2585,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent_not_running_notice_detail_description" : {
|
"agent_not_configured_notice_detail_description" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"af" : {
|
"af" : {
|
||||||
@@ -2797,7 +2602,7 @@
|
|||||||
},
|
},
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "L'agent segur (SecretAgent) és un procés que funciona en rerefons per signar peticions, tal que no hages de tindre Secretive obert tot el temps.\n\n**Secretive no funcionarà correctament a menys que l'agent estiga instal·lat i en funcionament.**"
|
"value" : "L'agent segur (SecretAgent) és un procés que funciona en rerefons per signar peticions, tal que no hages de tindre Secretive obert tot el temps.\n\n**Secretive no funcionarà correctament a menys que l'agent estiga instal·lat i en funcionament.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2815,20 +2620,20 @@
|
|||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent ist ein Hintergrund-Prozess, der Anfragen signiert, sodass Du Secretive nicht durchgehend geöffnet haben musst.\n\n**Secretive wird nicht richtig funktionieren, wenn der Agent nicht installiert und ausgeführt wird.**"
|
"value" : "SecretAgent ist ein Hintergrund-Prozess, der Anfragen signiert, sodass Du Secretive nicht durchgehend geöffnet haben musst.\n\n**Secretive wird nicht richtig funktionieren, wenn der Agent nicht installiert und ausgeführt wird.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"el" : {
|
"el" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Το SecretAgent είναι μια διεργασία που εκτελείται στο παρασκήνιο για να υπογράφει αιτήματα. Δεν χρειάζεται να κρατάτε παράθυρο του Secretive ανοιχτό συνεχώς.\n\n**Το Secretive δεν θα μπορεί να λειτουργήσει σωστά εκτός και αν o Agent είναι εγκατεστημένος και εκτελείται.**"
|
"value" : "Το SecretAgent είναι μια διεργασία που εκτελείται στο παρασκήνιο για να υπογράφει αιτήματα. Δεν χρειάζεται να κρατάτε παράθυρο του Secretive ανοιχτό συνεχώς.\n\n**Το Secretive δεν θα μπορεί να λειτουργήσει σωστά εκτός και αν o Agent είναι εγκατεστημένος και εκτελείται.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**Secretive will not be able to function properly unless the agent is installed and running.**"
|
"value" : "SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**Secretive will not be able to function properly unless the agent is configured.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
@@ -2839,13 +2644,13 @@
|
|||||||
},
|
},
|
||||||
"fi" : {
|
"fi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent-agenttiprosessi toimii taustalla ja allekirjoittaa pyyntöjä, jotta Secretive-käyttöliittymää ei tarvitse pitää aina auki.\n\n**Secretive ei toimi oikein, jollei agentti ole asennettu ja käynnissä.**"
|
"value" : "SecretAgent-agenttiprosessi toimii taustalla ja allekirjoittaa pyyntöjä, jotta Secretive-käyttöliittymää ei tarvitse pitää aina auki.\n\n**Secretive ei toimi oikein, jollei agentti ole asennettu ja käynnissä.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent est un processus qui s'exécute en arrière-plan pour signer les demandes, de sorte que vous n'ayez pas besoin de garder Secretive ouvert en permanence.\n\n**Secretive ne pourra pas fonctionner correctement sans que l'agent soit installé et fonctionne.**"
|
"value" : "SecretAgent est un processus qui s'exécute en arrière-plan pour signer les demandes, de sorte que vous n'ayez pas besoin de garder Secretive ouvert en permanence.\n\n**Secretive ne pourra pas fonctionner correctement sans que l'agent soit installé et fonctionne.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2869,13 +2674,13 @@
|
|||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgentはバックグラウンドで稼働し署名を行います。Secretiveアプリを常に実行する必要はありません。\n\n**Secretiveアプリはエージェントがインストールされて稼働しない限り正しく動作しません。**"
|
"value" : "SecretAgentはバックグラウンドで稼働し署名を行います。Secretiveアプリを常に実行する必要はありません。\n\n**Secretiveアプリはエージェントがインストールされて稼働しない限り正しく動作しません。**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent는 백그라운드에서 실행되어 요청에 서명하는 프로세스이므로 Secretive를 항상 열어 둘 필요는 없습니다.\n\n**Secretive 에이전트가 설치되어 실행 중이어야 Secretive가 제대로 작동합니다.**"
|
"value" : "SecretAgent는 백그라운드에서 실행되어 요청에 서명하는 프로세스이므로 Secretive를 항상 열어 둘 필요는 없습니다.\n\n**Secretive 에이전트가 설치되어 실행 중이어야 Secretive가 제대로 작동합니다.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2893,7 +2698,7 @@
|
|||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent to proces działający w tle, który podpisuje żądania, dzięki czemu nie musisz mieć otwartego programu Secretive przez cały czas.\n\n**Program Secretive nie będzie działał poprawnie, jeśli agent nie zostanie zainstalowany i uruchomiony.**"
|
"value" : "SecretAgent to proces działający w tle, który podpisuje żądania, dzięki czemu nie musisz mieć otwartego programu Secretive przez cały czas.\n\n**Program Secretive nie będzie działał poprawnie, jeśli agent nie zostanie zainstalowany i uruchomiony.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2905,7 +2710,7 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"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.**"
|
"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.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2917,7 +2722,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent это процесс, который работает в фоне чтобы подписывать запросы. Так Вам не придется все время держать Secretive открытым.\n\n**Secretive не сможет нормально функционировать, пока агент не установлен и не запущен.**"
|
"value" : "SecretAgent это процесс, который работает в фоне чтобы подписывать запросы. Так Вам не придется все время держать Secretive открытым.\n\n**Secretive не сможет нормально функционировать, пока агент не установлен и не запущен.**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2953,19 +2758,19 @@
|
|||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent是个在后台处理签名请求的进程,让您无需将Secretive一直保持在前台。\n\n**需要安装并运行Agent,否则Secretive无法正常工作。**"
|
"value" : "SecretAgent是个在后台处理签名请求的进程,让您无需将Secretive一直保持在前台。\n\n**需要安装并运行Agent,否则Secretive无法正常工作。**"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent是個在後台處理簽名請求的行程,讓您無需將Secretive一直保持在前台。\n\n**需要安裝並執行Agent,否則Secretive無法正常工作。**"
|
"value" : "SecretAgent是個在後台處理簽名請求的行程,讓您無需將Secretive一直保持在前台。\n\n**需要安裝並執行Agent,否則Secretive無法正常工作。**"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent_not_running_notice_title" : {
|
"agent_not_configured_notice_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"af" : {
|
"af" : {
|
||||||
@@ -2982,7 +2787,7 @@
|
|||||||
},
|
},
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent inactiu"
|
"value" : "Agent inactiu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3000,20 +2805,20 @@
|
|||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent wird Nicht Ausgeführt"
|
"value" : "Agent wird Nicht Ausgeführt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"el" : {
|
"el" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Ο Agent Δεν Εκτελείται"
|
"value" : "Ο Agent Δεν Εκτελείται"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Agent Is Not Running"
|
"value" : "Agent Is Not Configured"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
@@ -3024,13 +2829,13 @@
|
|||||||
},
|
},
|
||||||
"fi" : {
|
"fi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agentti ei ole käynnissä"
|
"value" : "Agentti ei ole käynnissä"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "L'agent n'est pas actif"
|
"value" : "L'agent n'est pas actif"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3048,19 +2853,19 @@
|
|||||||
},
|
},
|
||||||
"it" : {
|
"it" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secret Agent non è in esecuzione"
|
"value" : "Secret Agent non è in esecuzione"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "エージェントが稼働していません"
|
"value" : "エージェントが稼働していません"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent가 실행되고 있지 않음"
|
"value" : "Agent가 실행되고 있지 않음"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3078,7 +2883,7 @@
|
|||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent nie jest uruchomiony"
|
"value" : "Agent nie jest uruchomiony"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3090,7 +2895,7 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent não está rodando"
|
"value" : "Agent não está rodando"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3102,7 +2907,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Агент не запущен"
|
"value" : "Агент не запущен"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3138,19 +2943,19 @@
|
|||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent尚未运行"
|
"value" : "Agent尚未运行"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent尚未執行"
|
"value" : "Agent尚未執行"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent_running_notice_detail_description" : {
|
"agent_ready_notice_detail_description" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"af" : {
|
"af" : {
|
||||||
@@ -3335,7 +3140,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent_running_notice_detail_title" : {
|
"agent_ready_notice_detail_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"af" : {
|
"af" : {
|
||||||
@@ -3352,7 +3157,7 @@
|
|||||||
},
|
},
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "L'agent secret està actiu"
|
"value" : "L'agent secret està actiu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3370,20 +3175,20 @@
|
|||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secret Agent wird Ausgeführt"
|
"value" : "Secret Agent wird Ausgeführt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"el" : {
|
"el" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Ο Πράκτορας εκτελείται"
|
"value" : "Ο Πράκτορας εκτελείται"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Secret Agent is Running"
|
"value" : "Secret Agent is Ready"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
@@ -3394,13 +3199,13 @@
|
|||||||
},
|
},
|
||||||
"fi" : {
|
"fi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent on käynnissä"
|
"value" : "SecretAgent on käynnissä"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent est actif"
|
"value" : "SecretAgent est actif"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3418,19 +3223,19 @@
|
|||||||
},
|
},
|
||||||
"it" : {
|
"it" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secret Agent è in esecuzione"
|
"value" : "Secret Agent è in esecuzione"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "エージェントは稼働中"
|
"value" : "エージェントは稼働中"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secret Agent가 실행 중입니다."
|
"value" : "Secret Agent가 실행 중입니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3448,7 +3253,7 @@
|
|||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secret Agent jest uruchomiony"
|
"value" : "Secret Agent jest uruchomiony"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3460,7 +3265,7 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secret Agent está rodando"
|
"value" : "Secret Agent está rodando"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3472,7 +3277,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretAgent запущен"
|
"value" : "SecretAgent запущен"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3508,19 +3313,19 @@
|
|||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secret Agent运行中"
|
"value" : "Secret Agent运行中"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secret Agent執行中"
|
"value" : "Secret Agent執行中"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent_running_notice_title" : {
|
"agent_ready_notice_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"af" : {
|
"af" : {
|
||||||
@@ -3537,7 +3342,7 @@
|
|||||||
},
|
},
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent actiu"
|
"value" : "Agent actiu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3555,7 +3360,7 @@
|
|||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent wird Ausgeführt"
|
"value" : "Agent wird Ausgeführt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3567,8 +3372,8 @@
|
|||||||
},
|
},
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "translated",
|
||||||
"value" : "Agent is Running"
|
"value" : "Agent is Ready"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
@@ -3579,13 +3384,13 @@
|
|||||||
},
|
},
|
||||||
"fi" : {
|
"fi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agentti on käynnissä"
|
"value" : "Agentti on käynnissä"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "L'agent est actif"
|
"value" : "L'agent est actif"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3603,19 +3408,19 @@
|
|||||||
},
|
},
|
||||||
"it" : {
|
"it" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent è in esecuzione"
|
"value" : "Agent è in esecuzione"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "エージェントは稼働中"
|
"value" : "エージェントは稼働中"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent가 실행중"
|
"value" : "Agent가 실행중"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3633,7 +3438,7 @@
|
|||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent jest uruchomiony"
|
"value" : "Agent jest uruchomiony"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3645,7 +3450,7 @@
|
|||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent está rodando"
|
"value" : "Agent está rodando"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3657,7 +3462,7 @@
|
|||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Агент запущен"
|
"value" : "Агент запущен"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3693,13 +3498,13 @@
|
|||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent运行中"
|
"value" : "Agent运行中"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Agent執行中"
|
"value" : "Agent執行中"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19977,12 +19782,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Review" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Review All" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"secret_detail_certificate_path_label" : {
|
"secret_detail_certificate_path_label" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
|
|||||||
@@ -35,15 +35,13 @@ import XPCWrappers
|
|||||||
self.osVersion = osVersion
|
self.osVersion = osVersion
|
||||||
self.currentVersion = currentVersion
|
self.currentVersion = currentVersion
|
||||||
Task {
|
Task {
|
||||||
do {
|
if checkOnLaunch {
|
||||||
if checkOnLaunch {
|
try? await checkForUpdates()
|
||||||
try await checkForUpdates()
|
}
|
||||||
}
|
while !Task.isCancelled {
|
||||||
while !Task.isCancelled {
|
try? await Task.sleep(for: .seconds(Int(checkFrequency)))
|
||||||
try? await Task.sleep(for: .seconds(Int(checkFrequency)))
|
try? await checkForUpdates()
|
||||||
try await checkForUpdates()
|
}
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -21,16 +20,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,52 +99,15 @@ extension Agent {
|
|||||||
throw NoMatchingKeyError()
|
throw NoMatchingKeyError()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Agent offering witness chance to object")
|
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
||||||
do {
|
|
||||||
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
|
||||||
} catch {
|
|
||||||
logger.debug("Witness objected")
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
logger.debug("Witness did not object")
|
|
||||||
|
|
||||||
if secret.authenticationRequirement.required {
|
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance)
|
||||||
// Slow path, may block or suggest batching.
|
|
||||||
return try await signWithRequiredAuthentication(data: data, store: store, secret: secret, provenance: provenance)
|
|
||||||
} else {
|
|
||||||
// Fast path, no blocking/enqueing required
|
|
||||||
return try await signWithoutRequiredAuthentication(data: data, store: store, secret: secret, provenance: provenance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func signWithoutRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Data {
|
|
||||||
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance, context: nil)
|
|
||||||
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
||||||
try await witness?.witness(accessTo: secret, from: store, by: provenance, offerPersistence: false)
|
|
||||||
|
try await witness?.witness(accessTo: secret, from: store, by: provenance)
|
||||||
|
|
||||||
logger.debug("Agent signed request")
|
logger.debug("Agent signed request")
|
||||||
return signedData
|
|
||||||
}
|
|
||||||
|
|
||||||
func signWithRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Data {
|
|
||||||
// let context: any AuthenticationContextProtocol
|
|
||||||
// let offerPersistence: Bool
|
|
||||||
// if let existing = await authenticationHandler.existingAuthenticationContextProtocol(for: SignatureRequest(secret: secret, provenance: provenance)) {
|
|
||||||
// context = existing
|
|
||||||
// offerPersistence = false
|
|
||||||
// logger.debug("Using existing auth context")
|
|
||||||
// } else {
|
|
||||||
// context = authenticationHandler.createAuthenticationContext(for: SignatureRequest(secret: secret, provenance: provenance))
|
|
||||||
// offerPersistence = secret.authenticationRequirement.required
|
|
||||||
// logger.debug("Creating fresh auth context")
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let context = try await authenticationHandler.waitForAuthentication(for: SignatureRequest(secret: secret, provenance: provenance))
|
|
||||||
let result = try await store.sign(data: data, with: secret, for: provenance, context: context.laContext)
|
|
||||||
let signedData = signatureWriter.data(secret: secret, signature: result)
|
|
||||||
try await witness?.witness(accessTo: secret, from: store, by: provenance, offerPersistence: false) // FIXME: THIS
|
|
||||||
logger.debug("Agent signed request")
|
|
||||||
return signedData
|
return signedData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,185 +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.
|
|
||||||
/// - context: The LAContext used to authorize the persistent context.
|
|
||||||
/// - duration: The duration of the authorization context, in seconds.
|
|
||||||
init<SecretType: Secret>(secret: SecretType, context: LAContext, duration: TimeInterval) {
|
|
||||||
self.secret = AnySecret(secret)
|
|
||||||
self.laContext = context
|
|
||||||
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
|
|
||||||
self.validity = .time(monotonicExpiration: clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
init<SecretType: Secret>(secret: SecretType, context: LAContext, requestIDs: Set<UUID>) {
|
|
||||||
self.secret = AnySecret(secret)
|
|
||||||
self.laContext = context
|
|
||||||
self.validity = .requestIDs(requestIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
init<SecretType: Secret>(secret: SecretType, context: LAContext, requestID: UUID) {
|
|
||||||
self.secret = AnySecret(secret)
|
|
||||||
self.laContext = context
|
|
||||||
self.validity = .exclusive(requestID)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 actor AuthenticationHandler {
|
|
||||||
|
|
||||||
private var persistedContexts: [AnySecret: AuthenticationContext] = [:]
|
|
||||||
private var holdingRequests: Set<SignatureRequest> = []
|
|
||||||
private var activeTask: Task<Void, any Error>?
|
|
||||||
|
|
||||||
private var lastBatchAuthPresentation: Set<SignatureRequest>?
|
|
||||||
private var presentBatchAuth: (([[SignatureRequest]], @escaping @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)?
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setBatchAuthHandler(_ handler: @escaping (@Sendable ([[SignatureRequest]], @escaping @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)) {
|
|
||||||
self.presentBatchAuth = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
public func waitForAuthentication(for request: SignatureRequest) 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
|
|
||||||
}
|
|
||||||
holdingRequests.insert(request)
|
|
||||||
logger.log("Waiting for authentication for \(request.id)")
|
|
||||||
defer {
|
|
||||||
logger.log("Removed hold for \(request.id)")
|
|
||||||
holdingRequests.remove(request)
|
|
||||||
}
|
|
||||||
while holdingRequests.count > 1 {
|
|
||||||
if hasBatchableRequests, holdingRequests != lastBatchAuthPresentation {
|
|
||||||
logger.log("Batchable requests exist, cancelling existing auth prompt")
|
|
||||||
activeTask?.cancel()
|
|
||||||
lastBatchAuthPresentation = holdingRequests
|
|
||||||
logger.log("Requesting batch auth presentation")
|
|
||||||
try await presentBatchAuth?(batchableRequests, persistAuthentication(for:))
|
|
||||||
logger.log("Requested batch auth presentation")
|
|
||||||
}
|
|
||||||
if let preauthorized = existingAuthenticationContext(for: request) {
|
|
||||||
logger.log("Batch auth context found, proceededing with preauthorized context")
|
|
||||||
return preauthorized
|
|
||||||
} else {
|
|
||||||
logger.log("Waiting for batch request handling")
|
|
||||||
}
|
|
||||||
try await Task.sleep(for: .milliseconds(100))
|
|
||||||
}
|
|
||||||
let laContext = LAContext()
|
|
||||||
laContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: request.provenance.origin.displayName, secretName: request.secret.name))
|
|
||||||
laContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
|
||||||
let context = AuthenticationContext(secret: request.secret, context: laContext, requestID: request.id)
|
|
||||||
|
|
||||||
activeTask = Task {
|
|
||||||
logger.log("Beginning individual auth prompt")
|
|
||||||
try await Task.sleep(for: .seconds(1000))
|
|
||||||
// _ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason)
|
|
||||||
logger.log("Ended individual auth prompt")
|
|
||||||
}
|
|
||||||
_ = try await activeTask?.value
|
|
||||||
// TODO: Check something beyond cancellation? id?
|
|
||||||
// Is this okay? Do we always assume that a cancelled task will be the proceeded on?
|
|
||||||
if activeTask?.isCancelled ?? false {
|
|
||||||
logger.log("Auth prompt was cancelled, waiting for explicit auth")
|
|
||||||
// If we explicitly cancelled the task, hang on until we auth it.
|
|
||||||
while true {
|
|
||||||
if let preauthorized = existingAuthenticationContext(for: request) {
|
|
||||||
logger.log("Explicit auth context found")
|
|
||||||
return preauthorized
|
|
||||||
}
|
|
||||||
try await Task.sleep(for: .milliseconds(100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
private var batchableRequests: [[SignatureRequest]] {
|
|
||||||
holdingRequests.reduce(into: [:]) { partialResult, next in
|
|
||||||
partialResult[next.batchID, default: []].append(next)
|
|
||||||
}
|
|
||||||
.values
|
|
||||||
.map { $0.sorted() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var hasBatchableRequests: Bool {
|
|
||||||
guard presentBatchAuth != nil else { return false }
|
|
||||||
return batchableRequests.count < holdingRequests.count
|
|
||||||
}
|
|
||||||
|
|
||||||
private func existingAuthenticationContext(for request: SignatureRequest) -> (any AuthenticationContextProtocol)? {
|
|
||||||
guard let persisted = persistedContexts[request.secret], persisted.valid(for: request) else { return nil }
|
|
||||||
return persisted
|
|
||||||
}
|
|
||||||
|
|
||||||
public func persistAuthentication<SecretType: Secret>(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 = AuthenticationContext(secret: secret, context: newContext, duration: duration)
|
|
||||||
persistedContexts[AnySecret(secret)] = context
|
|
||||||
}
|
|
||||||
|
|
||||||
private func persistAuthentication(for requests: Set<SignatureRequest>) async throws {
|
|
||||||
activeTask?.cancel()
|
|
||||||
guard let first = requests.first else { return }
|
|
||||||
let newContext = LAContext()
|
|
||||||
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
|
||||||
|
|
||||||
newContext.localizedReason = String("Multiple")
|
|
||||||
// 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 = AuthenticationContext(secret: first.secret, context: newContext, requestIDs: Set(requests.map(\.id)))
|
|
||||||
persistedContexts[AnySecret(first.secret)] = context
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -17,6 +17,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, offerPersistence: Bool) async throws
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
import launch
|
||||||
|
|
||||||
/// A controller that manages socket configuration and request dispatching.
|
/// A controller that manages socket configuration and request dispatching.
|
||||||
public struct SocketController {
|
public struct SocketController {
|
||||||
@@ -12,7 +13,8 @@ public struct SocketController {
|
|||||||
private let sessionsContinuation: AsyncStream<Session>.Continuation
|
private let sessionsContinuation: AsyncStream<Session>.Continuation
|
||||||
|
|
||||||
/// The active SocketPort. Must be retained to be kept valid.
|
/// The active SocketPort. Must be retained to be kept valid.
|
||||||
private let port: SocketPort
|
/// Only applicable for legacy non-launchd sockets.
|
||||||
|
private let port: SocketPort?
|
||||||
|
|
||||||
/// The FileHandle for the main socket.
|
/// The FileHandle for the main socket.
|
||||||
private let fileHandle: FileHandle
|
private let fileHandle: FileHandle
|
||||||
@@ -23,23 +25,45 @@ public struct SocketController {
|
|||||||
/// Tracer which determines who originates a socket connection.
|
/// Tracer which determines who originates a socket connection.
|
||||||
private let requestTracer = SigningRequestTracer()
|
private let requestTracer = SigningRequestTracer()
|
||||||
|
|
||||||
/// Initializes a socket controller with a specified path.
|
public enum Socket {
|
||||||
/// - Parameter path: The path to use as a socket.
|
case launchd(String)
|
||||||
public init(path: String) {
|
case path(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(_ socket: Socket) {
|
||||||
(sessions, sessionsContinuation) = AsyncStream<Session>.makeStream()
|
(sessions, sessionsContinuation) = AsyncStream<Session>.makeStream()
|
||||||
logger.debug("Socket controller setting up at \(path)")
|
switch socket {
|
||||||
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
case .path(let path):
|
||||||
logger.debug("Socket controller removed existing socket")
|
logger.debug("Socket controller setting up at \(path)")
|
||||||
|
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
||||||
|
logger.debug("Socket controller removed existing socket")
|
||||||
|
}
|
||||||
|
let exists = FileManager.default.fileExists(atPath: path)
|
||||||
|
assert(!exists)
|
||||||
|
logger.debug("Socket controller path is clear")
|
||||||
|
let port = SocketPort(path: path)
|
||||||
|
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
||||||
|
self.port = port
|
||||||
|
logger.debug("Socket listening at \(path)")
|
||||||
|
case .launchd(let name):
|
||||||
|
logger.debug("Socket controller setting for launchd-controlled socket \(name)")
|
||||||
|
port = nil
|
||||||
|
var fileDescriptors: UnsafeMutablePointer<Int32>? = nil
|
||||||
|
var count = 0
|
||||||
|
let result = unsafe launch_activate_socket(name, &fileDescriptors, &count)
|
||||||
|
guard result == kOSReturnSuccess, let socket = unsafe fileDescriptors?.pointee else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
fileHandle = FileHandle(fileDescriptor: socket, closeOnDealloc: true)
|
||||||
}
|
}
|
||||||
let exists = FileManager.default.fileExists(atPath: path)
|
listen()
|
||||||
assert(!exists)
|
}
|
||||||
logger.debug("Socket controller path is clear")
|
|
||||||
port = SocketPort(path: path)
|
func listen() {
|
||||||
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
|
||||||
Task { @MainActor [fileHandle, sessionsContinuation, logger] in
|
Task { @MainActor [fileHandle, sessionsContinuation, logger] in
|
||||||
// Create the sequence before triggering the notification to
|
// Create the sequence before triggering the notification to
|
||||||
// ensure it will not be missed.
|
// ensure it will not be missed.
|
||||||
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted, object: fileHandle)
|
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted)
|
||||||
|
|
||||||
fileHandle.acceptConnectionInBackgroundAndNotify()
|
fileHandle.acceptConnectionInBackgroundAndNotify()
|
||||||
|
|
||||||
@@ -51,7 +75,7 @@ public struct SocketController {
|
|||||||
fileHandle.acceptConnectionInBackgroundAndNotify()
|
fileHandle.acceptConnectionInBackgroundAndNotify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug("Socket listening at \(path)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -125,14 +149,7 @@ private extension SocketPort {
|
|||||||
|
|
||||||
convenience init(path: String) {
|
convenience init(path: String) {
|
||||||
var addr = sockaddr_un()
|
var addr = sockaddr_un()
|
||||||
|
let length = addr.setPath(path)
|
||||||
let length = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
|
|
||||||
path.withCString { cstring in
|
|
||||||
let len = unsafe strlen(cstring)
|
|
||||||
unsafe strncpy(pointer, cstring, len)
|
|
||||||
return len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This doesn't seem to be _strictly_ neccessary with SocketPort.
|
// This doesn't seem to be _strictly_ neccessary with SocketPort.
|
||||||
// but just for good form.
|
// but just for good form.
|
||||||
addr.sun_family = sa_family_t(AF_UNIX)
|
addr.sun_family = sa_family_t(AF_UNIX)
|
||||||
@@ -144,3 +161,31 @@ private extension SocketPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension sockaddr_un {
|
||||||
|
|
||||||
|
mutating func setPath(_ path: String) -> Int {
|
||||||
|
#if compiler(<6.4)
|
||||||
|
unsafe withUnsafeMutablePointer(to: &self.sun_path.0) { pointer in
|
||||||
|
unsafe path.withCString { cstring in
|
||||||
|
let len = unsafe strlen(cstring)
|
||||||
|
unsafe strncpy(pointer, cstring, len)
|
||||||
|
return len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
withUnsafeMutablePointer(to: &self.sun_path.0) { pointer in
|
||||||
|
path.withCString { cstring in
|
||||||
|
let len = unsafe strlen(cstring)
|
||||||
|
unsafe strncpy(pointer, cstring, len)
|
||||||
|
return len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changes the header from `UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>?` -> `UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>!`
|
||||||
|
@_silgen_name("launch_activate_socket")
|
||||||
|
func launch_activate_socket(_ name: UnsafePointer<CChar>, _ fds: UnsafeMutablePointer<UnsafeMutablePointer<Int32>?>!, _ cnt: UnsafeMutablePointer<Int>!) -> Int32
|
||||||
|
|||||||
@@ -0,0 +1,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, LAContext?) async throws -> Data
|
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) 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, context: $3) }
|
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
||||||
|
_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, context: LAContext?) async throws -> Data {
|
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) async throws -> Data {
|
||||||
try await _sign(data, secret, provenance, context)
|
try await _sign(data, secret, provenance)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,41 +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 {
|
|
||||||
/// Whether the context remains valid.
|
|
||||||
|
|
||||||
var secret: AnySecret { get }
|
|
||||||
|
|
||||||
var laContext: LAContext { get }
|
|
||||||
|
|
||||||
func valid(for request: SignatureRequest) -> Bool
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct SignatureRequest: Identifiable, Hashable, Sendable, Comparable {
|
|
||||||
|
|
||||||
public let id: UUID
|
|
||||||
public let date: Date
|
|
||||||
public let secret: AnySecret
|
|
||||||
public let provenance: SigningRequestProvenance
|
|
||||||
|
|
||||||
public init(secret: AnySecret, provenance: SigningRequestProvenance) {
|
|
||||||
self.id = UUID()
|
|
||||||
self.date = Date()
|
|
||||||
self.secret = secret
|
|
||||||
self.provenance = provenance
|
|
||||||
}
|
|
||||||
|
|
||||||
public var batchID: Int {
|
|
||||||
var hasher = Hasher()
|
|
||||||
provenance.batchID.hash(into: &hasher)
|
|
||||||
secret.id.hash(into: &hasher)
|
|
||||||
return hasher.finalize()
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func < (lhs: SignatureRequest, rhs: SignatureRequest) -> Bool {
|
|
||||||
lhs.date < rhs.date
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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, context: LAContext?) async throws -> Data
|
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) 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
|
||||||
|
|||||||
@@ -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,16 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
// MARK: SecretStore
|
// MARK: SecretStore
|
||||||
|
|
||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
||||||
|
var context: LAContext
|
||||||
|
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
|
||||||
|
context = 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 queryAttributes = KeychainDictionary([
|
let queryAttributes = KeychainDictionary([
|
||||||
kSecClass: Constants.keyClass,
|
kSecClass: Constants.keyClass,
|
||||||
@@ -78,6 +88,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()
|
||||||
|
|||||||
@@ -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,15 +57,23 @@ extension SmartCard {
|
|||||||
|
|
||||||
// MARK: Public API
|
// MARK: Public API
|
||||||
|
|
||||||
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
||||||
public func sign(data: Data, with secret: SmartCard.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
|
|
||||||
guard let tokenID = await state.tokenID else { fatalError() }
|
guard let tokenID = await state.tokenID else { fatalError() }
|
||||||
|
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([
|
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!, // FIXME: THIS
|
kSecUseAuthenticationContext: context,
|
||||||
kSecReturnRef: true
|
kSecReturnRef: true
|
||||||
])
|
])
|
||||||
var untyped: CFTypeRef?
|
var untyped: CFTypeRef?
|
||||||
@@ -84,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()
|
||||||
|
|||||||
@@ -49,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, context: AuthenticationContextProtocol?) throws -> Data {
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
||||||
guard !shouldThrow else {
|
guard !shouldThrow else {
|
||||||
throw NSError(domain: "test", code: 0, userInfo: nil)
|
throw NSError(domain: "test", code: 0, userInfo: nil)
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ extension Stub {
|
|||||||
return try privateKey.signature(for: data).rawRepresentation
|
return try privateKey.signature(for: data).rawRepresentation
|
||||||
}
|
}
|
||||||
|
|
||||||
public func existingAuthenticationContextProtocol(secret: Stub.Secret) -> AuthenticationContextProtocol? {
|
public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,138 +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)
|
|
||||||
|
|
||||||
@State var pending: ([[SignatureRequest]], (Set<SignatureRequest>) async throws -> Void)?
|
|
||||||
@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()
|
|
||||||
for await message in session.messages {
|
|
||||||
let request = try await inputParser.parse(data: message)
|
|
||||||
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
|
|
||||||
try session.write(agentResponse)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
try? session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// .task {
|
|
||||||
// let socketController = SocketController(path: URL.agentHomeURL.appendingPathComponent("socket-two.ssh").path())
|
|
||||||
// let socketController = SocketController(path: "/Users/max/Downloads/test.ssh")
|
|
||||||
// let agent = Agent(storeList: storeList, authenticationHandler: authenticationHandler, witness: notifier)
|
|
||||||
// for await session in socketController.sessions {
|
|
||||||
// Task {
|
|
||||||
// let inputParser = try await XPCAgentInputParser()
|
|
||||||
// do {
|
|
||||||
// for await message in session.messages {
|
|
||||||
// let request = try await inputParser.parse(data: message)
|
|
||||||
// let agentResponse = await agent.handle(request: request, provenance: session.provenance)
|
|
||||||
// try 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 {
|
|
||||||
await authenticationHandler.setBatchAuthHandler { @MainActor pending, authorize in
|
|
||||||
self.pending = (pending, authorize)
|
|
||||||
openWindow(id: String(describing: BatchedRequestsView.self))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.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(id: String(describing: BatchedRequestsView.self)) {
|
|
||||||
pendingView
|
|
||||||
}
|
|
||||||
.windowStyle(.hiddenTitleBar)
|
|
||||||
.windowResizability(.contentSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
var pendingView: some View {
|
|
||||||
if let (requests, authorize) = pending {
|
|
||||||
BatchedRequestsView(pending: requests, review: authorize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
104
Sources/SecretAgent/AppDelegate.swift
Normal file
104
Sources/SecretAgent/AppDelegate.swift
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import Cocoa
|
||||||
|
import OSLog
|
||||||
|
import SecretKit
|
||||||
|
import SecureEnclaveSecretKit
|
||||||
|
import SmartCardSecretKit
|
||||||
|
import SecretAgentKit
|
||||||
|
import Brief
|
||||||
|
import Observation
|
||||||
|
import SSHProtocolKit
|
||||||
|
import CertificateKit
|
||||||
|
import Common
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
|
||||||
|
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
||||||
|
|
||||||
|
@MainActor var certificateStore: CertificateStore {
|
||||||
|
EnvironmentValues._certificateStore
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
@main
|
||||||
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
|
@MainActor private let storeList: SecretStoreList = {
|
||||||
|
let list = SecretStoreList()
|
||||||
|
let cryptoKit = SecureEnclave.Store()
|
||||||
|
let migrator = SecureEnclave.CryptoKitMigrator()
|
||||||
|
try? migrator.migrate(to: cryptoKit)
|
||||||
|
list.add(store: cryptoKit)
|
||||||
|
list.add(store: SmartCard.Store())
|
||||||
|
let certsMigrator = CertificateMigrator(homeDirectory: URL.homeDirectory, certificateStore: EnvironmentValues._certificateStore)
|
||||||
|
try? certsMigrator.migrate()
|
||||||
|
return list
|
||||||
|
}()
|
||||||
|
private let updater = Updater(checkOnLaunch: true)
|
||||||
|
private let notifier = Notifier()
|
||||||
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory)
|
||||||
|
@MainActor private lazy var agent: Agent = {
|
||||||
|
Agent(storeList: storeList, certificateStore: EnvironmentValues._certificateStore, witness: notifier)
|
||||||
|
}()
|
||||||
|
private var shutdownTask: Task<Void, Error>?
|
||||||
|
private let socketController = SocketController(.launchd("SecureListener"))
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
logger.debug("SecretAgent finished launching")
|
||||||
|
Task {
|
||||||
|
for await session in socketController.sessions {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let inputParser = try await XPCAgentInputParser()
|
||||||
|
for await message in session.messages {
|
||||||
|
let request = try await inputParser.parse(data: message)
|
||||||
|
let agentResponse = await agent.handle(request: request, provenance: session.provenance)
|
||||||
|
try session.write(agentResponse)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
try? session.close()
|
||||||
|
}
|
||||||
|
startCountdownClock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
|
||||||
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
for await _ in NotificationCenter.default.notifications(named: .certificateStoreReloaded) {
|
||||||
|
try? publicKeyFileStoreController.generateCertificates(for: EnvironmentValues._certificateStore.certificates, clear: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||||
|
try? publicKeyFileStoreController.generateCertificates(for: EnvironmentValues._certificateStore.certificates, clear: true)
|
||||||
|
notifier.prompt()
|
||||||
|
_ = withObservationTracking {
|
||||||
|
updater.update
|
||||||
|
} onChange: { [updater, notifier] in
|
||||||
|
Task {
|
||||||
|
guard !updater.currentVersion.isTestBuild else { return }
|
||||||
|
await notifier.notify(update: updater.update!) { release in
|
||||||
|
await updater.ignore(release: release)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCountdownClock() {
|
||||||
|
// FIXME: ACCOUNT FOR STORED AUTH
|
||||||
|
logger.log("Beginning countdown clock")
|
||||||
|
shutdownTask?.cancel()
|
||||||
|
shutdownTask = Task { [logger] in
|
||||||
|
try await Task.sleep(for: .seconds(30))
|
||||||
|
logger.log("Shutting down")
|
||||||
|
await NSApplication.shared.terminate(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import SecretKit
|
|
||||||
import SecretAgentKit
|
|
||||||
import SmartCardSecretKit
|
|
||||||
|
|
||||||
struct BatchedRequestsView: View {
|
|
||||||
|
|
||||||
let pending: [[SignatureRequest]]
|
|
||||||
let review: (Set<SignatureRequest>) async throws -> Void
|
|
||||||
|
|
||||||
init(pending: [[SignatureRequest]], review: @escaping (Set<SignatureRequest>) async throws -> Void) {
|
|
||||||
self.pending = pending
|
|
||||||
self.review = review
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
// .padding()
|
|
||||||
Form {
|
|
||||||
// Text("Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually.")
|
|
||||||
ForEach(Array(pending.enumerated()), id: \.offset) { group in
|
|
||||||
Section {
|
|
||||||
ForEach(Array(group.element.enumerated()), id: \.offset) { pending in
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(pending.element.provenance.origin.displayName)
|
|
||||||
.font(.headline)
|
|
||||||
Text(pending.element.provenance.date.formatted())
|
|
||||||
.font(.footnote)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Button("Review") {
|
|
||||||
Task {
|
|
||||||
try? await review([pending.element])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
HStack {
|
|
||||||
Text("\(group.element.first!.provenance.origin.displayName) - \(group.element.first!.secret.name)")
|
|
||||||
Spacer()
|
|
||||||
Button("Review All") {
|
|
||||||
Task {
|
|
||||||
try? await review(Set(group.element))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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, offerPersistence: Bool) async {
|
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) 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()
|
||||||
@@ -70,7 +69,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) {
|
||||||
@@ -104,8 +103,8 @@ extension Notifier: SigningWitness {
|
|||||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, offerPersistence: Bool) async throws {
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
|
||||||
await notify(accessTo: secret, from: store, by: provenance, offerPersistence: offerPersistence)
|
await notify(accessTo: secret, from: store, by: provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -134,24 +133,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) {
|
||||||
@@ -199,12 +202,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; };
|
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; };
|
||||||
50020BB024064869003D4025 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* App.swift */; };
|
50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; };
|
||||||
5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 5002C3AA2EEF483300FFAD22 /* XPCWrappers */; };
|
5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 5002C3AA2EEF483300FFAD22 /* XPCWrappers */; };
|
||||||
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; };
|
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; };
|
||||||
5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; };
|
5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; };
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
||||||
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
|
||||||
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
|
||||||
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
501421652781268000BBAA70 /* SecretAgent.app in Copy SecretAgent */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
|
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||||
503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503647472F870B7800977A23 /* BatchedRequestsView.swift */; };
|
502452F92FE2026E009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist in Copy SecretAgent plist */ = {isa = PBXBuildFile; fileRef = 502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */; };
|
||||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
||||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
|
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
|
||||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
|
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
|
||||||
@@ -182,6 +182,17 @@
|
|||||||
name = "Embed XPC Services";
|
name = "Embed XPC Services";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
502452F82FE2024D009EE753 /* Copy SecretAgent plist */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = Contents/Library/LaunchAgents;
|
||||||
|
dstSubfolderSpec = 1;
|
||||||
|
files = (
|
||||||
|
502452F92FE2026E009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist in Copy SecretAgent plist */,
|
||||||
|
);
|
||||||
|
name = "Copy SecretAgent plist";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
50617DBF23FCE4AB0099B055 /* Embed Frameworks */ = {
|
50617DBF23FCE4AB0099B055 /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -202,21 +213,22 @@
|
|||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
50C385AF240E438B00AF2719 /* CopyFiles */ = {
|
50C385AF240E438B00AF2719 /* Copy SecretAgent */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
dstPath = Contents/Library/LoginItems;
|
dstPath = Contents/Library/LoginItems;
|
||||||
dstSubfolderSpec = 1;
|
dstSubfolderSpec = 1;
|
||||||
files = (
|
files = (
|
||||||
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */,
|
501421652781268000BBAA70 /* SecretAgent.app in Copy SecretAgent */,
|
||||||
);
|
);
|
||||||
|
name = "Copy SecretAgent";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = "<group>"; };
|
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = "<group>"; };
|
||||||
50020BAF24064869003D4025 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
|
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
|
||||||
500666D02F04786900328939 /* SecretiveUpdater.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretiveUpdater.entitlements; sourceTree = "<group>"; };
|
500666D02F04786900328939 /* SecretiveUpdater.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretiveUpdater.entitlements; sourceTree = "<group>"; };
|
||||||
500666D12F04787200328939 /* SecretAgentInputParser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgentInputParser.entitlements; sourceTree = "<group>"; };
|
500666D12F04787200328939 /* SecretAgentInputParser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgentInputParser.entitlements; sourceTree = "<group>"; };
|
||||||
@@ -225,7 +237,7 @@
|
|||||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
||||||
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
|
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
|
||||||
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||||
503647472F870B7800977A23 /* BatchedRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchedRequestsView.swift; sourceTree = "<group>"; };
|
502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = com.maxgoedjen.Secretive.SecretAgent.plist; sourceTree = "<group>"; };
|
||||||
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
|
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
|
||||||
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
|
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
|
||||||
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
|
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
|
||||||
@@ -447,6 +459,7 @@
|
|||||||
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
|
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
|
||||||
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
|
50E4C4C72E777E4200C73783 /* AppIcon.icon */,
|
||||||
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
||||||
|
502452F32FE1FF89009EE753 /* com.maxgoedjen.Secretive.SecretAgent.plist */,
|
||||||
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
|
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
|
||||||
50617D8823FCE48E0099B055 /* Preview Content */,
|
50617D8823FCE48E0099B055 /* Preview Content */,
|
||||||
);
|
);
|
||||||
@@ -528,10 +541,9 @@
|
|||||||
50A3B78B24026B7500D209EA /* SecretAgent */ = {
|
50A3B78B24026B7500D209EA /* SecretAgent */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
50020BAF24064869003D4025 /* App.swift */,
|
50020BAF24064869003D4025 /* AppDelegate.swift */,
|
||||||
5018F54E24064786002EB505 /* Notifier.swift */,
|
5018F54E24064786002EB505 /* Notifier.swift */,
|
||||||
501578122E6C0479004A37D0 /* XPCInputParser.swift */,
|
501578122E6C0479004A37D0 /* XPCInputParser.swift */,
|
||||||
503647472F870B7800977A23 /* BatchedRequestsView.swift */,
|
|
||||||
50E2057F2FAB291E00402380 /* CertificateMigrator.swift */,
|
50E2057F2FAB291E00402380 /* CertificateMigrator.swift */,
|
||||||
50A3B79524026B7600D209EA /* Main.storyboard */,
|
50A3B79524026B7600D209EA /* Main.storyboard */,
|
||||||
50A3B79824026B7600D209EA /* Info.plist */,
|
50A3B79824026B7600D209EA /* Info.plist */,
|
||||||
@@ -572,7 +584,8 @@
|
|||||||
50617D7C23FCE48D0099B055 /* Frameworks */,
|
50617D7C23FCE48D0099B055 /* Frameworks */,
|
||||||
50617D7D23FCE48D0099B055 /* Resources */,
|
50617D7D23FCE48D0099B055 /* Resources */,
|
||||||
50617DBF23FCE4AB0099B055 /* Embed Frameworks */,
|
50617DBF23FCE4AB0099B055 /* Embed Frameworks */,
|
||||||
50C385AF240E438B00AF2719 /* CopyFiles */,
|
50C385AF240E438B00AF2719 /* Copy SecretAgent */,
|
||||||
|
502452F82FE2024D009EE753 /* Copy SecretAgent plist */,
|
||||||
501577C92E6BC5B4004A37D0 /* Embed XPC Services */,
|
501577C92E6BC5B4004A37D0 /* Embed XPC Services */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
@@ -870,7 +883,6 @@
|
|||||||
50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */,
|
50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */,
|
||||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
||||||
503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */,
|
|
||||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
|
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -1526,9 +1538,8 @@
|
|||||||
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1564,9 +1575,8 @@
|
|||||||
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1603,9 +1613,8 @@
|
|||||||
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_ENHANCED_SECURITY = YES;
|
ENABLE_ENHANCED_SECURITY = YES;
|
||||||
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "1"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
ReferencedContainer = "container:Secretive.xcodeproj">
|
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<MetalAPIValidationSettings
|
||||||
|
isEnabled = "No">
|
||||||
|
</MetalAPIValidationSettings>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
@unsafe @preconcurrency import ServiceManagement
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import SecureEnclaveSecretKit
|
import SecureEnclaveSecretKit
|
||||||
import SmartCardSecretKit
|
import SmartCardSecretKit
|
||||||
import Brief
|
import Brief
|
||||||
import CertificateKit
|
import CertificateKit
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
final class LaunchService: Sendable {
|
||||||
|
private let service = SMAppService.agent(plistName: "com.maxgoedjen.Secretive.SecretAgent.plist")
|
||||||
|
var status: SMAppService.Status {
|
||||||
|
service.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure() {
|
||||||
|
try? service.unregister()
|
||||||
|
try! service.register()
|
||||||
|
}
|
||||||
|
|
||||||
|
func disable() {
|
||||||
|
try? service.unregister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Secretive: App {
|
struct Secretive: App {
|
||||||
|
|
||||||
@Environment(\.agentLaunchController) var agentLaunchController
|
|
||||||
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
||||||
|
|
||||||
@SceneBuilder var body: some Scene {
|
@SceneBuilder var body: some Scene {
|
||||||
@@ -16,18 +33,8 @@ struct Secretive: App {
|
|||||||
ContentView()
|
ContentView()
|
||||||
.environment(EnvironmentValues._secretStoreList)
|
.environment(EnvironmentValues._secretStoreList)
|
||||||
.environment(EnvironmentValues._certificateStore)
|
.environment(EnvironmentValues._certificateStore)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
.onAppear {
|
||||||
Task {
|
EnvironmentValues._launchService.configure()
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
|
||||||
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
|
||||||
guard hasRunSetup && !explicitlyDisabled else { return }
|
|
||||||
agentLaunchController.check()
|
|
||||||
guard !agentLaunchController.developmentBuild else { return }
|
|
||||||
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
|
|
||||||
// Relaunch the agent, since it'll be running from earlier update still
|
|
||||||
try? await agentLaunchController.forceLaunch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.commands {
|
.commands {
|
||||||
@@ -115,6 +122,9 @@ extension EnvironmentValues {
|
|||||||
private static let _justUpdatedChecker = JustUpdatedChecker()
|
private static let _justUpdatedChecker = JustUpdatedChecker()
|
||||||
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
|
@Entry var justUpdatedChecker: any JustUpdatedCheckerProtocol = _justUpdatedChecker
|
||||||
|
|
||||||
|
fileprivate static let _launchService = LaunchService()
|
||||||
|
@Entry var launchService: LaunchService = _launchService
|
||||||
|
|
||||||
@MainActor var secretStoreList: SecretStoreList {
|
@MainActor var secretStoreList: SecretStoreList {
|
||||||
EnvironmentValues._secretStoreList
|
EnvironmentValues._secretStoreList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import LocalAuthentication
|
|
||||||
|
|
||||||
enum Preview {}
|
enum Preview {}
|
||||||
|
|
||||||
@@ -39,10 +38,17 @@ extension Preview {
|
|||||||
self.init(secrets: new)
|
self.init(secrets: new)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
|
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) 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 +82,16 @@ extension Preview {
|
|||||||
self.init(secrets: new)
|
self.init(secrets: new)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
|
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) 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() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import SwiftUI
|
|||||||
|
|
||||||
struct AgentStatusView: View {
|
struct AgentStatusView: View {
|
||||||
|
|
||||||
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
|
@Environment(\.launchService) private var launchService
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if agentLaunchController.running {
|
if launchService.status == .enabled {
|
||||||
AgentRunningView()
|
AgentRunningView()
|
||||||
} else {
|
} else {
|
||||||
AgentNotRunningView()
|
AgentNotRunningView()
|
||||||
@@ -14,54 +14,53 @@ struct AgentStatusView: View {
|
|||||||
}
|
}
|
||||||
struct AgentRunningView: View {
|
struct AgentRunningView: View {
|
||||||
|
|
||||||
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
|
@Environment(\.launchService) private var launchService
|
||||||
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
if let process = agentLaunchController.process {
|
// if let process = agentLaunchController.process {
|
||||||
ConfigurationItemView(
|
// ConfigurationItemView(
|
||||||
title: .agentDetailsLocationTitle,
|
// title: .agentDetailsLocationTitle,
|
||||||
value: process.bundleURL!.path(),
|
// value: process.bundleURL!.path(),
|
||||||
action: .revealInFinder(process.bundleURL!.path()),
|
// action: .revealInFinder(process.bundleURL!.path()),
|
||||||
)
|
// )
|
||||||
ConfigurationItemView(
|
ConfigurationItemView(
|
||||||
title: .agentDetailsSocketPathTitle,
|
title: .agentDetailsSocketPathTitle,
|
||||||
value: URL.socketPath,
|
value: URL.socketPath,
|
||||||
action: .copy(URL.socketPath),
|
action: .copy(URL.socketPath),
|
||||||
)
|
)
|
||||||
ConfigurationItemView(
|
// ConfigurationItemView(
|
||||||
title: .agentDetailsVersionTitle,
|
// title: .agentDetailsVersionTitle,
|
||||||
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
|
// value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
|
||||||
)
|
// )
|
||||||
if let launchDate = process.launchDate {
|
// if let launchDate = process.launchDate {
|
||||||
ConfigurationItemView(
|
// ConfigurationItemView(
|
||||||
title: .agentDetailsRunningSinceTitle,
|
// title: .agentDetailsRunningSinceTitle,
|
||||||
value: launchDate.formatted()
|
// value: launchDate.formatted()
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} header: {
|
} header: {
|
||||||
Text(.agentRunningNoticeDetailTitle)
|
Text(.agentReadyNoticeDetailTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
} footer: {
|
} footer: {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(.agentRunningNoticeDetailDescription)
|
Text(.agentReadyNoticeDetailDescription)
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Menu(.agentDetailsRestartAgentButton) {
|
Menu(.agentDetailsRestartAgentButton) {
|
||||||
Button(.agentDetailsDisableAgentButton) {
|
Button(.agentDetailsDisableAgentButton) {
|
||||||
Task {
|
Task {
|
||||||
explicitlyDisabled = true
|
explicitlyDisabled = true
|
||||||
try? await agentLaunchController
|
launchService.disable()
|
||||||
.uninstall()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} primaryAction: {
|
} primaryAction: {
|
||||||
Task {
|
Task {
|
||||||
try? await agentLaunchController.forceLaunch()
|
launchService.configure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +77,6 @@ struct AgentRunningView: View {
|
|||||||
|
|
||||||
struct AgentNotRunningView: View {
|
struct AgentNotRunningView: View {
|
||||||
|
|
||||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
|
||||||
@State var triedRestart = false
|
@State var triedRestart = false
|
||||||
@State var loading = false
|
@State var loading = false
|
||||||
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
@AppStorage("explicitlyDisabled") var explicitlyDisabled = false
|
||||||
@@ -87,12 +85,12 @@ struct AgentNotRunningView: View {
|
|||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
} header: {
|
} header: {
|
||||||
Text(.agentNotRunningNoticeTitle)
|
Text(.agentNotConfiguredNoticeTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
} footer: {
|
} footer: {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text(.agentNotRunningNoticeDetailDescription)
|
Text(.agentNotConfiguredNoticeDetailDescription)
|
||||||
HStack {
|
HStack {
|
||||||
if !triedRestart {
|
if !triedRestart {
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -100,14 +98,6 @@ struct AgentNotRunningView: View {
|
|||||||
explicitlyDisabled = false
|
explicitlyDisabled = false
|
||||||
guard !loading else { return }
|
guard !loading else { return }
|
||||||
loading = true
|
loading = true
|
||||||
Task {
|
|
||||||
try? await agentLaunchController.forceLaunch()
|
|
||||||
loading = false
|
|
||||||
|
|
||||||
if !agentLaunchController.running {
|
|
||||||
triedRestart = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
if !loading {
|
if !loading {
|
||||||
Text(.agentDetailsStartAgentButton)
|
Text(.agentDetailsStartAgentButton)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct ContentView: View {
|
|||||||
@Environment(\.certificateStore) private var certificateStore
|
@Environment(\.certificateStore) private var certificateStore
|
||||||
@Environment(\.updater) private var updater
|
@Environment(\.updater) private var updater
|
||||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
@Environment(\.agentLaunchController) private var agentLaunchController
|
||||||
|
@Environment(\.launchService) private var launchService
|
||||||
|
|
||||||
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
|
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
|
||||||
@State private var showingCreation = false
|
@State private var showingCreation = false
|
||||||
@@ -147,15 +148,15 @@ extension ContentView {
|
|||||||
showingAgentInfo = true
|
showingAgentInfo = true
|
||||||
}, label: {
|
}, label: {
|
||||||
HStack {
|
HStack {
|
||||||
if agentLaunchController.running {
|
if launchService.status == .enabled {
|
||||||
Text(.agentRunningNoticeTitle)
|
Text(.agentReadyNoticeTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||||
Circle()
|
Circle()
|
||||||
.frame(width: 10, height: 10)
|
.frame(width: 10, height: 10)
|
||||||
.foregroundColor(Color.green)
|
.foregroundColor(Color.green)
|
||||||
} else {
|
} else {
|
||||||
Text(.agentNotRunningNoticeTitle)
|
Text(.agentNotConfiguredNoticeTitle)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Circle()
|
Circle()
|
||||||
.frame(width: 10, height: 10)
|
.frame(width: 10, height: 10)
|
||||||
@@ -165,8 +166,8 @@ extension ContentView {
|
|||||||
})
|
})
|
||||||
.buttonStyle(
|
.buttonStyle(
|
||||||
ToolbarStatusButtonStyle(
|
ToolbarStatusButtonStyle(
|
||||||
lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
lightColor: launchService.status == .enabled ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||||
darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
darkColor: launchService.status == .enabled ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||||
|
|||||||
@@ -163,9 +163,10 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
|||||||
} else {
|
} else {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
content
|
content
|
||||||
|
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
|
||||||
|
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
|
||||||
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
|
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
|
||||||
.mask(RoundedRectangle(cornerRadius: 15))
|
.mask(RoundedRectangle(cornerRadius: 15))
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 15))
|
|
||||||
.shadow(color: .black.opacity(0.1), radius: 5)
|
.shadow(color: .black.opacity(0.1), radius: 5)
|
||||||
} else {
|
} else {
|
||||||
content
|
content
|
||||||
@@ -181,7 +182,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
|||||||
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
|
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
|
||||||
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:
|
||||||
|
|||||||
18
Sources/Secretive/com.maxgoedjen.Secretive.SecretAgent.plist
Normal file
18
Sources/Secretive/com.maxgoedjen.Secretive.SecretAgent.plist
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.maxgoedjen.Secretive.SecretAgent</string>
|
||||||
|
<key>BundleProgram</key>
|
||||||
|
<string>Contents/Library/LoginItems/SecretAgent.app/Contents/MacOS/SecretAgent</string>
|
||||||
|
<key>Sockets</key>
|
||||||
|
<dict>
|
||||||
|
<key>SecureListener</key>
|
||||||
|
<dict>
|
||||||
|
<key>SecureSocketWithKey</key>
|
||||||
|
<string>SECRETAGENT_SOCK</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Reference in New Issue
Block a user