Adds a "Download diagnostic report" button on the Health tab that
produces an age-encrypted .age file the user can attach to a GitHub
issue without exposing sensitive data.
Archive contents (tar.gz, then age-encrypted with the maintainer's
SSH ed25519 public key):
- diagnostic.json structured health/device summary (no secrets)
- datastore/…/*.xml raw on-disk XML verbatim for diff vs HTTP
- http/service/… live service HTTP responses per account/device
- http/speaker/… live speaker API responses (port 8090)
- ssh/speaker/… CA bundles + logread (last 20 min, 127.0.0.1
filtered) + dmesg fetched via SSH
- system/ca.pem service CA cert
- system/resolv.conf host DNS resolver config
- settings.json service settings (OAuth secrets redacted)
- env.txt filtered process environment
- logs/service.txt in-memory service log buffer
Supporting tooling:
- scripts/setup-diagnostic-key.sh one-time SSH key-pair generation
- scripts/decrypt-diagnostic.go go run helper for maintainer decryption
- keys/public/diagnostic.pub committed public key (matches github.com/gesellix.keys)
- docs/DIAGNOSTIC-EXPORT.md maintainer setup + user workflow guide
- docs/concepts/ENCRYPTED-EXPORT.md research notes and architecture rationale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.4 KiB
Encrypted Diagnostic Export
AfterTouch can produce an encrypted diagnostic report that users can download and
send to the project maintainer without exposing sensitive data to third parties.
The report is encrypted with an SSH public key using
age; only the holder of the matching
private key can read it.
What the report contains
The encrypted .age file decrypts to a .tar.gz archive with:
diagnostic.json— structured summary:- Service version and build info
- Full health-check results (same data as the Health tab)
- Per-device state: sources (IDs, names, SourceKeyTypes), presets (slot, name, Source, SourceID, location), device product code, firmware version, IP, name
datastore/accounts/{id}/devices/{id}/*.xml— raw XML files verbatim from the sender's datastore (Presets.xml,Sources.xml,Recents.xml, …)
Having both the structured JSON and the raw XML lets you compare what the service serves via HTTP against what is actually stored on disk.
What is excluded from the JSON: authentication tokens, credentials, OAuth secrets, Spotify refresh tokens. The raw XML files are included as-is.
Maintainer setup (one-time)
This section is for the project maintainer only. Users never need to touch keys.
1. Generate the key pair
bash scripts/setup-diagnostic-key.sh
This creates:
keys/private/diagnostic— SSH ed25519 private key (gitignored, never commit)keys/private/diagnostic.pub— copy for reference (gitignored)keys/public/diagnostic.pub— public key committed to the repo
2. Add the public key to GitHub
Go to https://github.com/settings/ssh/new and paste the contents of
keys/public/diagnostic.pub. This makes the key visible at
https://github.com/gesellix.keys so users can independently verify that the
key embedded in the binary matches a key actually controlled by the maintainer.
3. Embed the public key in the binary
Open pkg/service/export/encrypt.go and update the DiagnosticPublicKey
constant to match the new public key:
const DiagnosticPublicKey = "ssh-ed25519 AAAA... aftertouch-diagnostic@gesellix"
4. Commit
git add keys/public/diagnostic.pub pkg/service/export/encrypt.go
git commit -m "keys: add diagnostic SSH public key"
keys/private/ is .gitignored — the private key will not be committed.
5. Back up the private key
The private key is not stored in git. Keep a copy in a secure location
(password manager, encrypted USB drive, etc.). If it is lost, a new key pair
must be generated and the constant in encrypt.go updated.
Verifying the embedded key (users)
Users who want to confirm that the key embedded in their running binary matches the maintainer's GitHub SSH keys can run:
# Compare the raw key text — both should show the same line:
curl -s https://github.com/gesellix.keys
cat keys/public/diagnostic.pub
The key should appear verbatim in both outputs.
Decrypting a received report (maintainer)
When a user sends you an aftertouch-diagnostic-*.age file, use the helper
script (no extra tools needed — only Go and the private key). Run from the
repository root directory:
# Decrypt and extract in one step:
go run scripts/decrypt-diagnostic.go aftertouch-diagnostic-<timestamp>.age | tar xz
# Or decrypt to a .tar.gz first, then inspect:
go run scripts/decrypt-diagnostic.go aftertouch-diagnostic-<timestamp>.age > report.tar.gz
tar xzf report.tar.gz
# → diagnostic.json
# → datastore/accounts/{id}/devices/{id}/Presets.xml (and Sources.xml, Recents.xml, …)
The script uses only the filippo.io/age Go module — no separate age CLI
installation required.
User workflow
- Open the AfterTouch admin UI and go to the Health tab.
- Click Download diagnostic report.
- The browser downloads
aftertouch-diagnostic-<timestamp>.age. - Attach the file to the GitHub issue or send it via a direct channel.
The file is opaque binary — the user cannot read it. All they see is that the report was generated and downloaded.
Key rotation
If the private key is compromised or lost:
- Run
scripts/setup-diagnostic-key.sh(delete the oldkeys/private/diagnosticfirst). - Add the new public key to GitHub and remove the old one.
- Update
DiagnosticPublicKeyinencrypt.go. - Commit and tag a new release.
Old reports encrypted with the previous key cannot be decrypted with the new key.