mirror of
https://github.com/Anon-Planet/thgtoa.git
synced 2026-06-20 20:48:00 +02:00
Compare commits
89 Commits
v1.2.1
...
04f5036f3e
| Author | SHA1 | Date | |
|---|---|---|---|
| 04f5036f3e | |||
| f079b5aba8 | |||
| fc3763f85c | |||
| 4e26ae486b | |||
| 9dee4c7691 | |||
| 4579c5f14e | |||
| bf00272811 | |||
| ae1c293c6b | |||
| 45a8539a9e | |||
| cc5ad371a8 | |||
| 84a7ccbdd9 | |||
| 4eaca49a1c | |||
| c5e5ae48e1 | |||
| d1817e9049 | |||
| ede2a53437 | |||
| 91a77ed552 | |||
| 1c3cf75cf0 | |||
| 121be79cd8 | |||
| 3b550119a8 | |||
| c19389ce49 | |||
| aabcbac3d9 | |||
| e11a1eb1ce | |||
| df6cfbc94b | |||
| 095bb0d8be | |||
| 8b81081089 | |||
| ccc97461c9 | |||
| 8d74635d49 | |||
| f71e5e2a28 | |||
| 3e28ec19ad | |||
| 192da89138 | |||
| c658c354ee | |||
| 343ad7f037 | |||
| 85ea1fee66 | |||
| cdc54d8b3b | |||
| 823edbf4af | |||
| f9d4c17ac6 | |||
| 4fd0413f4b | |||
| bb005772af | |||
| 77840d7d5c | |||
| f02a15a07c | |||
| c0aa6b8814 | |||
| c6fd2891e0 | |||
| 11f88859bf | |||
| 68b2687b6b | |||
| 3184181fa8 | |||
| f3cb57230f | |||
| 5e8057bb1f | |||
| ac3d2ceb37 | |||
| 5eded0af38 | |||
| 1bb0acc3e8 | |||
| 25bc901ece | |||
| 78a0a37ee8 | |||
| aeb63cd7ba | |||
| 64ddd18535 | |||
| 7c9847e7d1 | |||
| 1e8c90513f | |||
| 2d09d7c01c | |||
| 1938e031ee | |||
| 8483d6336b | |||
| 1c168691c5 | |||
| ae50911375 | |||
| df2dd61676 | |||
| 904fa24478 | |||
| 28556c016c | |||
| 7bc3ed6bb6 | |||
| 9a58ca1b7c | |||
| 655e47fb8d | |||
| c0eb8aa6f3 | |||
| 90aa8b5442 | |||
| 85912692d2 | |||
| 6305e1fbbb | |||
| 0b71c3f49a | |||
| 468ff8f4a1 | |||
| 11c2882ba5 | |||
| 4c3ca7bfd7 | |||
| 5636291c8a | |||
| 41ac52de0a | |||
| f100633632 | |||
| 062128732e | |||
| e0d16797ed | |||
| d5659af3f7 | |||
| a2dbdd10e9 | |||
| cd00fa79fd | |||
| c49cc87390 | |||
| a14191bc7b | |||
| a47e02939d | |||
| 52a38f5deb | |||
| 206a6ff6b7 | |||
| 0e8de6ccc0 |
@@ -0,0 +1,9 @@
|
||||
[tool.commitizen]
|
||||
name = "cz_conventional_commits"
|
||||
|
||||
# enforce sign-off below as well
|
||||
extra_arguments = ["-S", "--signoff"]
|
||||
|
||||
# harmless redundancy
|
||||
[tool.commitizen.customize]
|
||||
schema_pattern = '^(feat|add|fix|bugfix|revert|security|perf|refactor|change|chore|ci|docs|style|test|build)(\(.+\))?(!)?: .{1,72}(\n.*)*\nSigned-off-by: .+ <.+@.+>'
|
||||
@@ -0,0 +1,85 @@
|
||||
# 1. Push to main → 01-build.yml runs automatically → note the run ID
|
||||
# 2. Manually trigger 02-sign.yml with that build run ID → note the sign run ID
|
||||
# 3. Manually trigger 03-release.yml with: version=v1.2.5, sign_run_id=<id>
|
||||
# 4. Manually trigger 04-changelog.yml with: version=v1.2.5
|
||||
|
||||
name: 📖 Build PDFs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_mode:
|
||||
description: 'PDF build mode'
|
||||
required: true
|
||||
default: 'both'
|
||||
type: choice
|
||||
options:
|
||||
- light
|
||||
- dark
|
||||
- both
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "mkdocs.yml"
|
||||
- "scripts/**"
|
||||
- ".github/workflows/01-build.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build PDFs
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
build_mode: ${{ steps.mode.outputs.build_mode }}
|
||||
run_id: ${{ github.run_id }}
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: 📦 Install Python dependencies
|
||||
run: pip install "mkdocs-material[imaging]" pillow numpy
|
||||
|
||||
- name: 🖼️ Install poppler and qpdf
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y poppler-utils qpdf
|
||||
|
||||
- name: Setup Chrome
|
||||
uses: browser-actions/setup-chrome@v2
|
||||
with:
|
||||
chrome-version: 120
|
||||
install-dependencies: true
|
||||
install-chromedriver: true
|
||||
|
||||
- name: 🖨️ Resolve build mode
|
||||
id: mode
|
||||
run: |
|
||||
MODE="${{ inputs.build_mode || 'both' }}"
|
||||
echo "build_mode=$MODE" >> $GITHUB_OUTPUT
|
||||
echo "Building in mode: $MODE"
|
||||
|
||||
- name: 🖨️ Build PDFs
|
||||
env:
|
||||
CI: true
|
||||
run: python scripts/build_guide_pdf.py --${{ steps.mode.outputs.build_mode }}
|
||||
|
||||
- name: 📤 Upload PDF artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pdfs
|
||||
path: |
|
||||
export/thgtoa.pdf
|
||||
export/thgtoa-dark.pdf
|
||||
if-no-files-found: warn
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
@@ -0,0 +1,256 @@
|
||||
name: 🔏 Sign PDFs
|
||||
|
||||
# Can be triggered:
|
||||
# 1. Automatically after build.yml completes on main
|
||||
# 2. Manually, pointing at a specific build run to pull PDFs from
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_run_id:
|
||||
description: 'build.yml run ID to download PDFs from'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# Download artifacts from other runs + commit export/ files back to the repo
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sign:
|
||||
name: Hash & Sign PDFs
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
light_sha256: ${{ steps.hashes.outputs.light_sha256 }}
|
||||
dark_sha256: ${{ steps.hashes.outputs.dark_sha256 }}
|
||||
light_b2: ${{ steps.hashes.outputs.light_b2 }}
|
||||
dark_b2: ${{ steps.hashes.outputs.dark_b2 }}
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout (for pgp/ key reference only)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: pgp
|
||||
|
||||
- name: 📥 Download PDF artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pdfs
|
||||
path: export/
|
||||
run-id: ${{ inputs.build_run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📋 List downloaded files
|
||||
run: ls -lh export/
|
||||
|
||||
# Hash - extensions match export/ conventions: .sha256, .b2sum
|
||||
- name: "#️⃣ Hash PDFs"
|
||||
id: hashes
|
||||
run: |
|
||||
cd export
|
||||
|
||||
for f in thgtoa.pdf thgtoa-dark.pdf; do
|
||||
[ -f "$f" ] || continue
|
||||
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
||||
b2sum "$f" | awk '{print $1}' > "${f}.b2sum"
|
||||
done
|
||||
|
||||
# Combined summary files
|
||||
sha256sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > sha256sums.txt || \
|
||||
sha256sum thgtoa.pdf 2>/dev/null > sha256sums.txt
|
||||
b2sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > b2sums.txt || \
|
||||
b2sum thgtoa.pdf 2>/dev/null > b2sums.txt
|
||||
|
||||
light_sha256=$(cat thgtoa.pdf.sha256 2>/dev/null || echo "")
|
||||
dark_sha256=$(cat thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
||||
light_b2=$(cat thgtoa.pdf.b2sum 2>/dev/null || echo "")
|
||||
dark_b2=$(cat thgtoa-dark.pdf.b2sum 2>/dev/null || echo "")
|
||||
|
||||
echo "light_sha256=$light_sha256" >> $GITHUB_OUTPUT
|
||||
echo "dark_sha256=$dark_sha256" >> $GITHUB_OUTPUT
|
||||
echo "light_b2=$light_b2" >> $GITHUB_OUTPUT
|
||||
echo "dark_b2=$dark_b2" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "--- SHA-256 ---"
|
||||
cat sha256sums.txt
|
||||
echo "--- BLAKE2b ---"
|
||||
cat b2sums.txt
|
||||
|
||||
# GPG sign — detached ASCII-armor signatures use .asc extension
|
||||
- name: 🔑 Install GPG
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y gnupg
|
||||
|
||||
- name: 🔏 Import GPG signing key
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback --list-secret-keys
|
||||
|
||||
- name: 🔏 Sign PDFs and hash files
|
||||
env:
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: |
|
||||
sign() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] || return 0
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback \
|
||||
--detach-sign --armor --output "${file}.asc" "$file"
|
||||
echo "Signed: $file → ${file}.asc"
|
||||
}
|
||||
sign export/thgtoa.pdf
|
||||
sign export/thgtoa-dark.pdf
|
||||
sign export/sha256sums.txt
|
||||
sign export/b2sums.txt
|
||||
|
||||
# Commit export/ back to main
|
||||
- name: 📦 Checkout full repo for commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
path: repo
|
||||
|
||||
- name: 📂 Copy export files into repo
|
||||
run: cp -v export/* repo/export/
|
||||
|
||||
- name: 🔏 Configure SSH commit signing
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.ACTIONS_SSH_SIGNING_KEY }}" > ~/.ssh/signing_key
|
||||
chmod 600 ~/.ssh/signing_key
|
||||
git config --global gpg.format ssh
|
||||
git config --global user.signingKey ~/.ssh/signing_key
|
||||
git config --global commit.gpgSign true
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# If no change in git diff, do nothing
|
||||
- name: 📤 Commit and push export/ to main
|
||||
working-directory: repo
|
||||
run: |
|
||||
git add export/
|
||||
if git diff --cached --quiet; then
|
||||
echo "Nothing to commit — export/ is already up to date."
|
||||
else
|
||||
git commit -S -m "chore(export): update PDFs, hashes and signatures [skip ci]"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
# Upload artifacts for 03-release.yml and verify job to consume
|
||||
- name: 📤 Upload signatures artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signatures
|
||||
path: |
|
||||
export/sha256sums.txt
|
||||
export/b2sums.txt
|
||||
export/thgtoa.pdf.sha256
|
||||
export/thgtoa-dark.pdf.sha256
|
||||
export/thgtoa.pdf.b2sum
|
||||
export/thgtoa-dark.pdf.b2sum
|
||||
export/thgtoa.pdf.asc
|
||||
export/thgtoa-dark.pdf.asc
|
||||
export/sha256sums.txt.asc
|
||||
export/b2sums.txt.asc
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
|
||||
- name: 📤 Upload signed PDFs artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pdfs-signed
|
||||
path: |
|
||||
export/thgtoa.pdf
|
||||
export/thgtoa-dark.pdf
|
||||
if-no-files-found: warn
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
|
||||
# Verify — runs after sign, surfaces results as a job summary
|
||||
verify:
|
||||
name: Verify hashes & signatures
|
||||
runs-on: ubuntu-latest
|
||||
needs: sign
|
||||
# Always run so the summary is visible even if sign partially failed
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout scripts and public key
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
scripts/verify_pdf.py
|
||||
pgp
|
||||
|
||||
- name: 📥 Download signatures artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: signatures
|
||||
path: export/
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download signed PDFs artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pdfs-signed
|
||||
path: export/
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🔑 Install GPG and import public key
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y gnupg
|
||||
gpg --import pgp/anonymousplanet-release.asc
|
||||
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: 🔍 Run verify_pdf.py
|
||||
id: verify
|
||||
run: |
|
||||
# Capture output and exit code separately so we can write the
|
||||
# summary regardless of whether verification passed or failed.
|
||||
set +e
|
||||
output=$(python scripts/verify_pdf.py --hashes --signatures --export-dir export 2>&1)
|
||||
exit_code=$?
|
||||
set -e
|
||||
|
||||
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||
|
||||
# ── Job summary ──────────────────────────────────────────────
|
||||
{
|
||||
if [ "$exit_code" -eq 0 ]; then
|
||||
echo "## ✅ Verification passed"
|
||||
else
|
||||
echo "## ❌ Verification failed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "**Run:** [\`${{ github.run_id }}\`](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) · **Commit:** [\`${GITHUB_SHA::7}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) · **By:** \`${{ github.actor }}\`"
|
||||
echo ""
|
||||
echo "### Script output"
|
||||
echo '```'
|
||||
echo "$output"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "### Hashes"
|
||||
echo '```'
|
||||
echo "── SHA-256 ──────────────────────────────────────────────────────"
|
||||
cat export/sha256sums.txt 2>/dev/null || echo "(not found)"
|
||||
echo ""
|
||||
echo "── BLAKE2b ──────────────────────────────────────────────────────"
|
||||
cat export/b2sums.txt 2>/dev/null || echo "(not found)"
|
||||
echo '```'
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Propagate failure so the job is marked red if verification fails
|
||||
exit $exit_code
|
||||
@@ -0,0 +1,191 @@
|
||||
name: 🚀 Release
|
||||
|
||||
# Manual only — run this deliberately after build and sign are confirmed good.
|
||||
# Provide the 02-sign.yml run ID to pull artifacts from. The release tag is
|
||||
# automatically passed to the tag input. Exports "inputs.version" to $TAG.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign_run_id:
|
||||
description: '02-sign.yml run ID to pull signatures and PDFs from'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Mark as pre-release?'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
version:
|
||||
description: 'Version string to record (e.g. v1.2.4) — required'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write # create releases and tags
|
||||
actions: read # download artifacts from other runs
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout (for pgp/)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
sparse-checkout: pgp
|
||||
|
||||
# Download artifacts from the specified sign run
|
||||
- name: 📥 Download signatures artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: signatures
|
||||
path: release/
|
||||
run-id: ${{ inputs.sign_run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download signed PDFs artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pdfs-signed
|
||||
path: release/
|
||||
run-id: ${{ inputs.sign_run_id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📋 List release assets
|
||||
run: ls -lh release/
|
||||
|
||||
# Read hashes for the release body
|
||||
- name: "#️⃣ Read hashes"
|
||||
id: hashes
|
||||
run: |
|
||||
read_hash() { cat "release/$1" 2>/dev/null || echo "(not built)"; }
|
||||
echo "light_sha256=$(read_hash thgtoa.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||
echo "dark_sha256=$(read_hash thgtoa-dark.pdf.sha256)" >> $GITHUB_OUTPUT
|
||||
echo "light_b2=$(read_hash thgtoa.pdf.b2sum)" >> $GITHUB_OUTPUT
|
||||
echo "dark_b2=$(read_hash thgtoa-dark.pdf.b2sum)" >> $GITHUB_OUTPUT
|
||||
|
||||
# VirusTotal
|
||||
- name: 🦠 Upload PDFs to VirusTotal
|
||||
id: vt
|
||||
uses: crazy-max/ghaction-virustotal@v5
|
||||
with:
|
||||
vt_api_key: ${{ secrets.VT_API_KEY }}
|
||||
files: |
|
||||
release/thgtoa.pdf
|
||||
release/thgtoa-dark.pdf
|
||||
|
||||
- name: 🔗 Build VT report URLs
|
||||
id: vt_urls
|
||||
run: |
|
||||
light_hash=$(cat release/thgtoa.pdf.sha256 2>/dev/null || echo "")
|
||||
dark_hash=$(cat release/thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
|
||||
if [ -n "$light_hash" ]; then
|
||||
echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "light_vt=(not built)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [ -n "$dark_hash" ]; then
|
||||
echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "dark_vt=(not built)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Generate release tag — timestamp + short SHA, always unique
|
||||
- name: 🏷️ Generate release tag
|
||||
id: tag
|
||||
run: |
|
||||
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
DATE=$(date -u +'%Y%m%d')
|
||||
TAG="${{ inputs.version }}"
|
||||
NAME="Release ${DATE} (${SHORT_SHA})"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
echo "Tag: $TAG"
|
||||
|
||||
# Create GitHub Release
|
||||
- name: 🚀 Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
name: ${{ steps.tag.outputs.name }}
|
||||
prerelease: ${{ inputs.prerelease || false }}
|
||||
draft: true
|
||||
fail_on_unmatched_files: false
|
||||
body: |
|
||||
## 📖 The Hitchhiker's Guide to Online Anonymity
|
||||
|
||||
Built from [`${{ inputs.version }}`](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.version }}).
|
||||
|
||||
---
|
||||
|
||||
### 📄 Release assets
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `thgtoa.pdf` | Light mode PDF |
|
||||
| `thgtoa-dark.pdf` | Dark mode PDF (hacker theme) |
|
||||
| `sha256sums.txt` | SHA-256 checksums (both files) |
|
||||
| `b2sums.txt` | BLAKE2b checksums (both files) |
|
||||
| `thgtoa.pdf.sha256` | SHA-256 — light PDF |
|
||||
| `thgtoa-dark.pdf.sha256` | SHA-256 — dark PDF |
|
||||
| `thgtoa.pdf.b2sum` | BLAKE2b — light PDF |
|
||||
| `thgtoa-dark.pdf.b2sum` | BLAKE2b — dark PDF |
|
||||
| `*.asc` | GPG detached signatures (ASCII armor) |
|
||||
|
||||
---
|
||||
|
||||
### #️⃣ Hashes
|
||||
|
||||
**thgtoa.pdf** (light)
|
||||
```text
|
||||
SHA-256 ${{ steps.hashes.outputs.light_sha256 }}
|
||||
BLAKE2b ${{ steps.hashes.outputs.light_b2 }}
|
||||
```
|
||||
|
||||
**thgtoa-dark.pdf** (dark)
|
||||
```text
|
||||
SHA-256 ${{ steps.hashes.outputs.dark_sha256 }}
|
||||
BLAKE2b ${{ steps.hashes.outputs.dark_b2 }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔏 Verifying GPG signatures
|
||||
|
||||
```sh
|
||||
# Import the release signing key
|
||||
gpg --import pgp/anonymousplanet-release.asc
|
||||
|
||||
# Verify PDFs
|
||||
gpg --verify thgtoa.pdf.asc thgtoa.pdf
|
||||
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
|
||||
|
||||
# Verify hash files
|
||||
gpg --verify sha256sums.txt.asc sha256sums.txt
|
||||
gpg --verify b2sums.txt.asc b2sums.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🦠 VirusTotal scans
|
||||
|
||||
| File | Report |
|
||||
|------|--------|
|
||||
| `thgtoa.pdf` | ${{ steps.vt_urls.outputs.light_vt }} |
|
||||
| `thgtoa-dark.pdf` | ${{ steps.vt_urls.outputs.dark_vt }} |
|
||||
|
||||
files: |
|
||||
release/thgtoa.pdf
|
||||
release/thgtoa-dark.pdf
|
||||
release/sha256sums.txt
|
||||
release/b2sums.txt
|
||||
release/thgtoa.pdf.sha256
|
||||
release/thgtoa-dark.pdf.sha256
|
||||
release/thgtoa.pdf.b2sum
|
||||
release/thgtoa-dark.pdf.b2sum
|
||||
release/thgtoa.pdf.asc
|
||||
release/thgtoa-dark.pdf.asc
|
||||
release/sha256sums.txt.asc
|
||||
release/b2sums.txt.asc
|
||||
@@ -0,0 +1,58 @@
|
||||
name: 📝 Update Changelog
|
||||
|
||||
# Manual only — run after a release is published. Provide the exact version
|
||||
# string (e.g. v1.2.4) to prepend to the changelog. Version is required to
|
||||
# prevent silent auto-increment drift from release tags.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version string to record (e.g. v1.2.4) — required'
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: 'Dry run — print entry without committing'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write # commit changelog back to main
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
name: Prepend changelog entry
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Use a PAT so the commit triggers downstream workflows (GITHUB_TOKEN won't)
|
||||
token: ${{ secrets.CHANGELOG_PAT || secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: 📝 Generate and prepend changelog entry
|
||||
env:
|
||||
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||
MANUAL_VERSION: ${{ inputs.version }}
|
||||
GH_SHA: ${{ github.sha }}
|
||||
GH_REF: ${{ github.ref_name }}
|
||||
TRIGGERING_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
run: python scripts/update_changelog.py
|
||||
|
||||
- name: 📤 Commit changelog
|
||||
if: ${{ inputs.dry_run != 'true' }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add docs/changelog/index.md
|
||||
# Only commit if there's actually a change
|
||||
git diff --cached --quiet && echo "No changelog change to commit." || \
|
||||
git commit -m "docs: update changelog [skip ci]"
|
||||
git push
|
||||
@@ -1,55 +0,0 @@
|
||||
name: 🚀 Build guide PDF
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "mkdocs.yml"
|
||||
- "scripts/build_guide_pdf.py"
|
||||
- ".github/workflows/build-pdf.yml"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "mkdocs.yml"
|
||||
- "scripts/build_guide_pdf.py"
|
||||
- ".github/workflows/build-pdf.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pdf:
|
||||
name: MkDocs + print to PDF
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install MkDocs Material
|
||||
run: pip install mkdocs-material
|
||||
|
||||
- name: Install Chromium
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends chromium
|
||||
|
||||
- name: Build PDF
|
||||
env:
|
||||
CI: true
|
||||
run: python scripts/build_guide_pdf.py
|
||||
|
||||
- name: Upload PDF artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: guide-pdf
|
||||
path: export/guide.pdf
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
@@ -14,7 +14,6 @@ jobs:
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
# Or use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to build without the mkdocs-material theme
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CUSTOM_DOMAIN: anonymousplanet.org
|
||||
CUSTOM_DOMAIN: anonymousplanet.net
|
||||
|
||||
+29
-22
@@ -1,22 +1,29 @@
|
||||
# Visual Studio (Windows) solution metadata
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
# Python (MkDocs, scripts/build_guide_pdf.py)
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.env
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
|
||||
# MkDocs build output and local PDF export
|
||||
site/
|
||||
_site/
|
||||
_site_test/
|
||||
export/
|
||||
# Visual Studio (Windows) solution metadata
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
# Python (MkDocs, scripts/build_guide_pdf.py)
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.env
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
|
||||
# MkDocs build output and local PDF export
|
||||
site/
|
||||
_site/
|
||||
_site_test/
|
||||
build/
|
||||
|
||||
# Export directory — track only hashes and signatures, not the PDFs themselves
|
||||
export/thgtoa.pdf.sha256
|
||||
export/thgtoa-dark.pdf.sha256
|
||||
export/thgtoa.pdf.b2sum
|
||||
export/thgtoa-dark.pdf.b2sum
|
||||
export/*.asc
|
||||
|
||||
+1
-18
@@ -62,24 +62,7 @@ MD012:
|
||||
# Consecutive blank lines
|
||||
maximum: 1
|
||||
# MD013/line-length - Line length
|
||||
#
|
||||
MD013:
|
||||
# Number of characters
|
||||
line_length: 80
|
||||
# Number of characters for headings
|
||||
heading_line_length: 80
|
||||
# Number of characters for code blocks
|
||||
code_block_line_length: 160
|
||||
# Include code blocks
|
||||
code_blocks: false
|
||||
# Include tables
|
||||
tables: false
|
||||
# Include headings
|
||||
headings: true
|
||||
# Strict length checking (e.g. allow for longer URLs)
|
||||
strict: false
|
||||
# Stern length checking
|
||||
stern: false
|
||||
MD013: false
|
||||
|
||||
# MD014/commands-show-output - Dollar signs used before commands without showing output
|
||||
# TODO: set false for now but we should consider enabling it
|
||||
|
||||
+12
-5
@@ -10,14 +10,21 @@ repos:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: check-symlinks
|
||||
- id: detect-private-key
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=lf]
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.41.0
|
||||
- repo: https://github.com/commitizen-tools/commitizen
|
||||
rev: v4.8.3
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
- id: markdownlint-fix
|
||||
- id: commitizen
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: require-commit-body
|
||||
name: Avoid bad commits
|
||||
language: script
|
||||
entry: scripts/hooks/require-commit-body.sh
|
||||
stages: [commit-msg]
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.1] - 2026-04-11
|
||||
|
||||
### Added
|
||||
|
||||
- GitHub Actions workflow **Build guide PDF** (`.github/workflows/build-pdf.yml`): installs Chromium on `ubuntu-latest`, runs `scripts/build_guide_pdf.py`, uploads `export/guide.pdf` as the `guide-pdf` artifact. Runs on `workflow_dispatch`, on pushes to `main` that touch docs or build inputs, and on matching pull requests.
|
||||
|
||||
- `scripts/build_guide_pdf.py` to build the MkDocs site and render the guide to a single PDF (`export/guide.pdf` by default) using a Chromium-based browser (Chrome or Edge) headless print-to-PDF.
|
||||
- `docs/stylesheets/extra.css` and `extra_css` in `mkdocs.yml` for shared site styling.
|
||||
- This `CHANGELOG.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
- `README.md` “Ways to read or export the guide”: hosted link, local `mkdocs serve`, PDF build via the script, ODT note, raw Markdown link.
|
||||
- Guide landing layout: wrap the opening block in `docs/guide/index.md` with a `guide-intro-lead` container so the logo and first sections share one layout context for web and print.
|
||||
- `.gitignore` to exclude local build outputs `export/`, `site/`, and `_site_test/`.
|
||||
- `scripts/build_guide_pdf.py`: when the `CI` environment variable is set, pass Chromium flags (`--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage`) so headless print works on typical CI images.
|
||||
- `README.md`: note the **Build guide PDF** GitHub Actions workflow and the `guide-pdf` artifact.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `docs/guide/index.md`: replace broken reference-style internal links (`[label][label:]`) with working same-page fragment links to the correct headings; correct the mismatched “Real-Name System” cross-reference; fix a broken footnote marker on the “free (unallocated) space of your hard drive” list item.
|
||||
|
||||
[Unreleased]: https://github.com/Anon-Planet/thgtoa/compare/v1.2.1...HEAD
|
||||
[1.2.1]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.1
|
||||
@@ -0,0 +1,427 @@
|
||||
Attribution-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
@@ -1,38 +1,26 @@
|
||||
<div align="center">
|
||||
<a href="https://anonymousplanet.net">
|
||||
<img src="https://anonymousplanet.net/media/profile.png"></a>
|
||||
|
||||
<p><em>The comprehensive guide for online anonymity and OpSec.</em></p>
|
||||
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/Anon-Planet?style=social"></a>
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/releases/latest">
|
||||
<img src="https://img.shields.io/badge/MkDocs_Material-526CFE?logo=MaterialForMkDocs&logoColor=green"></a>
|
||||
<a href="https://github.com/Anon-Planet/thgtoa/actions/workflows/release.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/Anon-Planet/thgtoa/release.yml?label=release"></a></p>
|
||||
</div>
|
||||
|
||||
Welcome.
|
||||
|
||||
**[IMPORTANT RECOMMENDATION FOR UKRAINIANS. ВАЖЛИВА РЕКОМЕНДАЦІЯ ДЛЯ УКРАЇНЦІВ](briar.html)**
|
||||
This is a guide with the aim of providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. <span style="color: red">**It is written with hope for activists, journalists, scientists, lawyers, whistle-blowers, and good people being oppressed, censored, harassed anywhere!**</span> This guide has no affiliation with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.com/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> collective/movement.
|
||||
|
||||
This is a maintained guide with the aim of providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. <span style="color: red">**It is written with hope for activists, journalists, scientists, lawyers, whistle-blowers, and good people being oppressed, censored, harassed anywhere!**</span> This guide has no affiliation with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.com/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> collective/movement.
|
||||
|
||||
This guide is an open-source non-profit initiative, [licensed](LICENSE.html) under **Creative Commons Attribution-NonCommercial 4.0 International** ([cc-by-nc-4.0](https://creativecommons.org/licenses/by-nc/4.0/) <sup>[[Archive.org]](https://web.archive.org/web/https://creativecommons.org/licenses/by-nc/4.0/)</sup>) and is **not sponsored/endorsed by any commercial/governmental entity**. This means that you are free to use our guide for pretty much any purpose **excluding commercially** as long as you do attribute it. There are no ads or any affiliate links.
|
||||
This guide is an open-source non-profit initiative, [licensed](LICENSE.html) under **Creative Commons Attribution-ShareAlike 4.0 International** ([cc-by-sa-4.0](https://creativecommons.org/licenses/by-sa/4.0/) <sup>[[Archive.org]](https://web.archive.org/web/https://creativecommons.org/licenses/by-sa/4.0/)</sup>) and is **not sponsored/endorsed by any commercial/governmental entity**. This means that you are free to use our guide for pretty much any purpose **excluding commercially** as long as you do attribute it. There are no ads or any affiliate links.
|
||||
|
||||
**If you would like to make a donation to help this project, you can do so from [here](donations.html) where you will also find the project goals. All the donations will be strictly used within the context of this project. All donations and spendings are logged on the donations page.**
|
||||
|
||||
**Ways to read or export the guide**
|
||||
|
||||
- **In your browser:** [Hitchhiker's Guide](https://www.anonymousplanet.org/guide/) (hosted site). After a local build you can also open `site/guide/index.html` directly.
|
||||
- **In your browser:** [Hitchhiker's Guide](https://anonymousplanet.net/) (hosted site). After a local build you can also open `site/guide/index.html` directly.
|
||||
- **Local HTML preview:** from the repository root, with Python 3 and [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/) installed (`pip install mkdocs-material`), run `mkdocs serve` and open the URL printed in the terminal (for example `http://127.0.0.1:8000`).
|
||||
- **PDF (local build):** from the repository root, using the same environment, run:
|
||||
|
||||
```bash
|
||||
python scripts/build_guide_pdf.py
|
||||
```
|
||||
|
||||
This runs `mkdocs build` (output defaults to `./site`), then uses **Google Chrome** or **Microsoft Edge** in headless mode to print `site/guide/index.html` to **`export/guide.pdf`** (images and styling preserved). If the site is already built: `python scripts/build_guide_pdf.py --skip-mkdocs`. Other options: `--site-dir`, `--pdf`, and `python scripts/build_guide_pdf.py --help`.
|
||||
|
||||
On **GitHub Actions**, the [Build guide PDF](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) workflow does the same using Chromium on Ubuntu when you push to `main` or open a pull request that touches the guide or build inputs; download the **`guide-pdf`** artifact from a successful run. You can also run it manually (**Actions** → **Build guide PDF** → **Run workflow**).
|
||||
- **OpenDocument (ODT):** not produced by this repository (previous hosted export removed).
|
||||
- **Raw Markdown (very large):** [docs/guide/index.md on GitHub](https://raw.githubusercontent.com/Anon-Planet/thgtoa/refs/heads/main/docs/guide/index.md)
|
||||
|
||||
**Mirrors:**
|
||||
- <del>Hidden service: <http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/></del> **Host down**
|
||||
|
||||
Feel free to submit issues using Github Issues with the repository link above. Criticism, opinions, and ideas are welcome!
|
||||
|
||||
**Follow or contact us on:**
|
||||
|
||||
Discussion Channels:
|
||||
- Matrix room: <https://matrix.to/#/#anonymity:anonymousplanet.net>
|
||||
- Matrix space: <https://matrix.to/#/#psa:anonymousplanet.net>
|
||||
|
||||
Have a good read and feel free to share and/or recommend it!
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
anonymousplanet.org
|
||||
anonymousplanet.net
|
||||
|
||||
+102
-114
@@ -1,114 +1,102 @@
|
||||
---
|
||||
title: "About Anonymous Planet"
|
||||
description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://www.anonymousplanet.org/
|
||||
name: Anonymous Planet
|
||||
url: https://www.anonymousplanet.org/about/
|
||||
logo: ../media/favicon.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
{ align=right }
|
||||
|
||||
**Anonymous Planet** are the maintainers of the [_Hitchhiker's Guide_](https://anonymousplanet.org/guide.html) and the [_PSA Community_](https://psa.anonymousplanet.org). It is responsible for maintaining the projects and code repositories.
|
||||
|
||||
The purpose: providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. It is written with the hopes that good people (e.g., activists, journalists, scientists, lawyers, whistle-blowers, etc.) will be able to fight oppression, censorship and harassment! The website and projects are free (as in freedom) and not affiliated with any donor or projects discussed.
|
||||
|
||||
??? Note "Where do I start?"
|
||||
|
||||
Start either by going to [the beginning](../guide/index.md) or using the search at top right of the page. It is also available at whatever point you are in your reading.
|
||||
|
||||
??? Note "Notes on the journey"
|
||||
|
||||
This guide is a work in progress. It will probably never be "finished". You may (will) find broken links when you click on some search results and during some navigation steps. Please report these. Otherwise, most of the search functionality is a great experience and can help you find linked topics. Try to search for something in one section of the reading. It will show up in many other places.
|
||||
|
||||
??? Note "Disclaimer"
|
||||
|
||||
There might be some wrong or outdated information in this guide because no one is perfect. Your experience may vary. Remember, check regularly for an updated version of this guide. Please do your own independent, well-thought research. There is no one resource online that can provide 100% security, anonymity, and/or privacy.
|
||||
|
||||
This guide is a non-profit open-source initiative, licensed under Creative Commons **Attribution-NonCommercial** 4.0 International ([cc-by-nc-4.0](https://creativecommons.org/licenses/by-nc/4.0/) <sup>[[Archive.org]](https://web.archive.org/web/https://creativecommons.org/licenses/by-nc/4.0/)</sup>).
|
||||
|
||||
- For mirrors see [Mirrors](../mirrors/index.md) and the links at the bottom right of the page. You should see these on every page.
|
||||
|
||||
- For help in comparing versions see [Comparing versions](../guide/index.md#appendix-a6-comparing-versions)
|
||||
|
||||
Feel free to submit issues **(please do report anything wrong)** using GitHub Issues at: <https://github.com/Anon-Planet/thgtoa/issues>. We also accept Merge Requests (MR) from our Gitlab and many other places. Do not hesitate to report issues and suggestions!
|
||||
|
||||
??? Note "Discuss ideas on Matrix for real-time chat"
|
||||
|
||||
We offer a Matrix.org hosted space of our own. Check it out!
|
||||
|
||||
- Read [the rules](https://psa.anonymousplanet.org/), please
|
||||
- Matrix Room: https://matrix.to/#/#nth:anonymousplanet.net
|
||||
- Matrix Space: https://matrix.to/#/#psa:anonymousplanet.net
|
||||
- Admins: @daskolburn:thomcat.rocks and @thehidden:tchncs.de
|
||||
|
||||
Follow us on:
|
||||
|
||||
- Twitter at <https://twitter.com/AnonyPla>
|
||||
|
||||
- Mastodon at <https://mastodon.social/@anonymousplanet>
|
||||
|
||||
To contact me, see the updated information on the website or send an e-mail to <contact@anonymousplanet.org>
|
||||
|
||||
**Please consider [donating](../guide/index.md#donations) if you enjoy the project and want to support the hosting fees or support the funding of initiatives like the hosting of Tor Exit Nodes.**
|
||||
|
||||
### Recommended Reading
|
||||
|
||||
Some of those resources may, in order to sustain their project, contain or propose:
|
||||
|
||||
- Sponsored commercial content
|
||||
- Monetized content through third party platforms (such as YouTube)
|
||||
- Affiliate links to commercial services
|
||||
- Paid Services such as consultancy
|
||||
- Premium content such as ad-free content or updated content
|
||||
- Merchandising
|
||||
|
||||
_Note that these websites could contain affiliate/sponsored content and/or merchandising. This guide does not endorse and is not sponsored by any commercial entity in any way._
|
||||
|
||||
If you skipped those, you should really still consider viewing this YouTube playlist from the Techlore Go Incognito project (<https://github.com/techlore-official/go-incognito> <sup>[[Archive.org]](https://web.archive.org/web/https://github.com/techlore-official/go-incognito)</sup>) as an introduction before going further: <https://www.youtube.com/playlist?list=PL3KeV6Ui_4CayDGHw64OFXEPHgXLkrtJO> <sup>[[Invidious]](https://yewtu.be/playlist?list=PL3KeV6Ui_4CayDGHw64OFXEPHgXLkrtJO)</sup>. This guide will cover many of the topics in the videos of this playlist with more details and references as well as some added topics not covered within that series. This will just take you 2 or 3 hours to watch it all.
|
||||
|
||||
_Anonymous Planet_ **does not** participate in any sponsoring, endorsement, advertising, or other affiliate programs for any entity. We only rely on anonymous donations in a closed, transparent loop system.
|
||||
|
||||
??? Note "Privacy related"
|
||||
|
||||
- AnarSec: <https://www.anarsec.guide/>
|
||||
- EFF Surveillance Self-Defense: <https://ssd.eff.org/>
|
||||
- Prism-Break: <https://prism-break.org/>
|
||||
- Privacy Guides: <https://privacyguides.org>
|
||||
- Techlore: <https://techlore.tech>
|
||||
- The New Oil: <https://thenewoil.org>
|
||||
- PrivacyTools.io: <https://privacytools.io>
|
||||
|
||||
??? Note "Blogs and personal websites"
|
||||
|
||||
- CIA Officer's Blog: <https://officercia.mirror.xyz/>
|
||||
- Continuing Ed: <https://edwardsnowden.substack.com/>
|
||||
- Madaidan's Insecurities: <https://madaidans-insecurities.github.io/>
|
||||
- Seirdy's Home: <https://seirdy.one/>
|
||||
|
||||
??? Note "Useful resources"
|
||||
|
||||
- KYC? Not me: <https://kycnot.me/>
|
||||
- Library Genesis: <https://en.wikipedia.org/wiki/Library_Genesis> <sup>[[Wikiless]](https://wikiless.com/wiki/Library_Genesis)</sup> (see their latest known URL in the Wikipedia article)
|
||||
- Real World Onion Sites: <https://github.com/alecmuffett/real-world-onion-sites>
|
||||
- Sci-Hub <https://en.wikipedia.org/wiki/Sci-Hub> <sup>[[Wikiless]](https://wikiless.com/wiki/Sci-Hub)</sup> (see their latest known URL in the main Wikipedia article)
|
||||
- Terms of Service, Didn't Read: <https://tosdr.org>
|
||||
- Whonix Documentation: <https://www.whonix.org/wiki/Documentation>
|
||||
|
||||
??? Note "We are not affiliated with Anonymous or Riseup"
|
||||
|
||||
One or two of our community members uses or has used the resources of Riseup. We are not affiliated with Riseup in any manner.
|
||||
|
||||
We also hold **no affiliation** with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.com/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> hacker collective.
|
||||
|
||||
## License
|
||||
|
||||
!!! Danger ""
|
||||
|
||||
:fontawesome-brands-creative-commons: :fontawesome-brands-creative-commons-by: :fontawesome-brands-creative-commons-nd: This guide is an open-source non-profit initiative, licensed under [Creative Commons Attribution-NonCommercial 4.0 International](https://github.com/Anon-Planet/thgtoa/blob/master/LICENSE.md) and is not sponsored/endorsed by any commercial/governmental entity. This means that you are free to use our guide for pretty much any purpose excluding commercially as long as you do attribute it. There are no ads or any affiliate links.
|
||||
---
|
||||
title: "Anonymous Planet"
|
||||
description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://anonymousplanet.net/about/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
|
||||
**Anonymous Planet** are the maintainers of [_The Hitchhiker's Guide_](../guide/index.md) and the [_PSA Community_](https://psa.anonymousplanet.net). This project is part of our ongoing efforts to provide open-source tools and resources for the community, made by people with extensive knowledge in signals and forensics, and expertise in various distributions of Linux. We are providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. It is written with the hopes that good people (e.g., activists, journalists, scientists, lawyers, whistle-blowers, etc.) will be able to fight oppression, censorship and harassment! The website and projects are free (as in freedom). We are high at risk individuals. We are not simply hackers with a bunch of recommendations and affiliate links like privacytools. To be blunt like Linus Torvalds with a bullshit Merge Request, we don't do that. Here, you will find a trove of information compiled into a single, cohesive set of instructions and sub-guides.
|
||||
|
||||
??? Note "Where do I start?"
|
||||
|
||||
Start either by going to [the beginning](../guide/index.md) or using the search at top right of the page. It is also available at whatever point you are in your reading.
|
||||
|
||||
??? Note "Notes on the journey"
|
||||
|
||||
This guide is a work in progress. It will probably never be "finished". You may find broken links when you click on some search results and during some navigation steps. Please report these. Otherwise, most of the search functionality is a great experience and can help you find linked topics. Try to search for something in one section of the reading. It will show up in many other places.
|
||||
|
||||
??? Note "Disclaimer"
|
||||
|
||||
There might be some wrong or outdated information in this guide because no one is perfect. Your experience may vary. Remember, check regularly for an updated version of this guide. Please do your own independent, well-thought research. There is no one resource online that can provide 100% security, anonymity, and/or privacy.
|
||||
|
||||
- For mirrors see [Mirrors](../mirrors/index.md) and the links at the bottom right of the page. You should see these on every page.
|
||||
|
||||
- For help in comparing versions see [Comparing versions](../guide/index.md#appendix-a6-comparing-versions)
|
||||
|
||||
Feel free to submit issues **(please do report anything wrong)** using GitHub Issues at: <https://github.com/Anon-Planet/thgtoa/issues>. We also accept Merge Requests (MR) from our Gitlab and many other places. Do not hesitate to report issues and suggestions!
|
||||
|
||||
??? tip "Discuss ideas on Matrix for real-time chat"
|
||||
|
||||
We offer a Matrix.org hosted space of our own. Check it out!
|
||||
|
||||
- Read [the rules](https://psa.anonymousplanet.net/), please
|
||||
- Matrix Room: https://matrix.to/#/#nth:anonymousplanet.net
|
||||
- Matrix Space: https://matrix.to/#/#psa:anonymousplanet.net
|
||||
- Admins: @daskolburn:thomcat.rocks and @thehidden:tchncs.de
|
||||
|
||||
???+ tip "Follow us on"
|
||||
|
||||
- Twitter at <https://twitter.com/AnonyPla>
|
||||
- Mastodon at <https://mastodon.social/@anonymousplanet>
|
||||
|
||||
To contact me, see the updated information on the website or send an e-mail to <contact@anonymousplanet.net>
|
||||
|
||||
**Please consider [donating](../guide/index.md#donations) if you enjoy the project and want to support the hosting fees or support the funding of initiatives like the hosting of Tor Exit Nodes.**
|
||||
|
||||
???+ example "Recommended Reading"
|
||||
|
||||
Some of those resources may, in order to sustain their project, contain or propose:
|
||||
|
||||
- Sponsored commercial content
|
||||
- Monetized content through third party platforms (such as YouTube)
|
||||
- Affiliate links to commercial services
|
||||
- Paid Services such as consultancy
|
||||
- Premium content such as ad-free content or updated content
|
||||
- Merchandising
|
||||
|
||||
_Note that these websites could contain affiliate/sponsored content and/or merchandising. This guide does not endorse and is not sponsored by any commercial entity in any way._
|
||||
|
||||
If you skipped those, you should really still consider viewing this YouTube playlist from the Techlore Go Incognito project (<https://github.com/techlore-official/go-incognito> <sup>[[Archive.org]](https://web.archive.org/web/https://github.com/techlore-official/go-incognito)</sup>) as an introduction before going further: <https://www.youtube.com/playlist?list=PL3KeV6Ui_4CayDGHw64OFXEPHgXLkrtJO> <sup>[[Invidious]](https://yewtu.be/playlist?list=PL3KeV6Ui_4CayDGHw64OFXEPHgXLkrtJO)</sup>. This guide will cover many of the topics in the videos of this playlist with more details and references as well as some added topics not covered within that series. This will just take you 2 or 3 hours to watch it all.
|
||||
|
||||
_Anonymous Planet_ **does not** participate in any sponsoring, endorsement, advertising, or other affiliate programs for any entity. We only rely on anonymous donations in a closed, transparent loop system.
|
||||
|
||||
??? tip "Privacy related"
|
||||
|
||||
- AnarSec: <https://www.anarsec.guide/>
|
||||
- EFF Surveillance Self-Defense: <https://ssd.eff.org/>
|
||||
- Prism-Break: <https://prism-break.org/>
|
||||
- Privacy Guides: <https://privacyguides.org>
|
||||
- Techlore: <https://techlore.tech>
|
||||
- The New Oil: <https://thenewoil.org>
|
||||
- PrivacyTools.io: <https://privacytools.io>
|
||||
|
||||
??? tip "Blogs and personal websites"
|
||||
|
||||
- CIA Officer's Blog: <https://officercia.mirror.xyz/>
|
||||
- Continuing Ed: <https://edwardsnowden.substack.com/>
|
||||
- Madaidan's Insecurities: <https://madaidans-insecurities.github.io/>
|
||||
- Seirdy's Home: <https://seirdy.one/>
|
||||
|
||||
??? tip "Useful resources"
|
||||
|
||||
- KYC? Not me: <https://kycnot.me/>
|
||||
- Library Genesis: <https://en.wikipedia.org/wiki/Library_Genesis> <sup>[[Wikiless]](https://wikiless.tiekoetter.com/wiki/Library_Genesis)</sup> (see their latest known URL in the Wikipedia article)
|
||||
- Real World Onion Sites: <https://github.com/alecmuffett/real-world-onion-sites>
|
||||
- Sci-Hub <https://en.wikipedia.org/wiki/Sci-Hub> <sup>[[Wikiless]](https://wikiless.tiekoetter.com/wiki/Sci-Hub)</sup> (see their latest known URL in the main Wikipedia article)
|
||||
- Terms of Service, Didn't Read: <https://tosdr.org>
|
||||
- Whonix Documentation: <https://www.whonix.org/wiki/Documentation> <sup>[[Archive.org]](https://web.archive.org/web/https://www.whonix.org/wiki/Documentation)</sup>
|
||||
|
||||
??? note "We are not affiliated with Anonymous or Riseup"
|
||||
|
||||
One or two of our community members uses or has used the resources of Riseup. We are not affiliated with Riseup in any manner.
|
||||
|
||||
We also hold **no affiliation** with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.tiekoetter.com/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> hacker collective.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: "Release Notes"
|
||||
description: "Release Notes"
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://anonymousplanet.net/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
|
||||
Notable changes to the guide. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
---
|
||||
|
||||
## [v1.2.5]
|
||||
|
||||
!!! Note "Meta"
|
||||
|
||||
- Website theme customization
|
||||
- License change to accomodate our fiscal host Open Source Collective
|
||||
|
||||
!!! Note "Changed"
|
||||
|
||||
- Comprehensive updates throughout the guide reflecting the transition to Whonix 18.x as the newest version
|
||||
- Replaced outdated Whonix.org wiki docs links with GitHub releases mirror and Archive.org backups
|
||||
- Updated Virtualbox hardening instructions (e.g., network time desync offsets, Spectre/Meltdown mitigations)
|
||||
- Added AppArmor enabling guidance for Debian-based Whonix 18.x Workstation VMs
|
||||
- Upgraded download instructions to use GitHub Releases as primary source with Archive.org fallback
|
||||
- Added new "Whonix Improvements" section documenting key changes from 17.x to 18.x (automated release upgrade, improved Qubes integration)
|
||||
- Updated Qubes OS compatibility notes: Whonix 18.x officially supported on R4.3+, Whonix 17.x compatible with R4.2+
|
||||
- Enhanced system verification instructions after upgrades (checkvm, tor --verify)
|
||||
- Improved documentation links to use clean markdown format with Archive.org mirrors for resilience
|
||||
- Added comprehensive upgrade path guidance with backup procedures
|
||||
- Updated 196! Wikipedia reference links throughout the entire guide
|
||||
|
||||
!!! Note "Added"
|
||||
|
||||
- All Whonix.org/wiki links now have Archive.org mirror backups for availability
|
||||
- Upgrade path documented: Whonix 17 to 18 with automated release-upgrade support
|
||||
- Detailed Qubes OS compatibility notes for both Whonix versions
|
||||
- You can now get the Anonymous Planet PGP keyring from the site (copy/paste or download)
|
||||
|
||||
!!! Note "Improved"
|
||||
|
||||
- Virtualbox hardening section reorganized
|
||||
- AppArmor configuration guidance added where applicable (Whonix 18.x)
|
||||
- KVM alternative documented in Appendix N for Linux users seeking better security than VirtualBox
|
||||
- Whonix Improvements subsection added before "Pick your guest workstation" VMs section
|
||||
|
||||
## [v1.2.4]
|
||||
|
||||
!!! Note "Meta"
|
||||
|
||||
- Rename workflows (GH - now we can know the order)
|
||||
|
||||
!!! Note "Changed"
|
||||
|
||||
- Change the repo URL for our tor mirror
|
||||
- Fix recommended reading admonition
|
||||
- Refactoring some things and removing others
|
||||
- More meta changes to the pipeline
|
||||
- Rewrite developer guide for current pipeline
|
||||
|
||||
!!! Note "Fixed"
|
||||
|
||||
- Fix an inline reference
|
||||
- Use the Anonymous Planet RSK for releases (we used the MSK for testing)
|
||||
- Prevent history dump and filter noise commits
|
||||
- Actually save per-page PDFs for qpdf, not PNGs
|
||||
- Fail fast with helpful message if pdftoppm or qpdf missing
|
||||
|
||||
## [v1.2.3]
|
||||
|
||||
CI/CD pipeline split into independent stages, dark PDF quality improved, release signing automated, and the changelog now updates itself on every build. Skipping v1.2.2 which was a placeholder and contained broken Python unsuitable for a tag/release.
|
||||
|
||||
???+ tip "Added"
|
||||
|
||||
- **Dark mode PDF** (`scripts/convert.py`): pixel-level converter replaces the broken `--prefers-color-scheme=dark` Chromium flag. Produces a 200 DPI hacker-themed PDF (`#1f1f31` background, `#e0e0e0` text, `#5e8bde` links) with batched page processing to avoid OOM on large documents.
|
||||
- **Three independent CI workflows** replacing the old monolithic `build-sign-release.yml`:
|
||||
- `01-build.yml`: builds PDFs and uploads them as an artifact; no secrets required, can be re-run freely.
|
||||
- `02-sign.yml`: downloads the PDF artifact, computes SHA-256 and BLAKE2b hashes, GPG-signs all outputs, and uploads a `signatures` artifact. Can be re-run against any historical build.
|
||||
- `03-release.yml`: downloads both artifacts, uploads to VirusTotal, and publishes a tagged GitHub Release with all 12 assets attached. Can be triggered manually against any previous sign run.
|
||||
- **`scripts/update_changelog.py`**: reads `git log` since the last version tag, categorises commits by conventional-commit prefix, and prepends a new entry to this file automatically after each successful build.
|
||||
- **`04-changelog.yml`** workflow: commits the auto-generated changelog entry back to `main` after every build, with `dry_run` and `manual_version` dispatch inputs for safe local testing.
|
||||
- **`scripts/tag_release.py`**: interactive guided helper for maintainers to create GPG-signed annotated tags. Checks clean tree and branch, auto-increments the version, pulls the message from the changelog, resolves the release signing key, creates and verifies the tag, then prints the push command.
|
||||
- **`docs/code/develop.md`**: full developer reference covering prerequisites, local build instructions, the pipeline flow, all required GitHub Secrets, the release process, verification steps, and a troubleshooting section for every known CI failure mode.
|
||||
|
||||
!!! warning "Changed"
|
||||
|
||||
- `build-sign-release.yml` deprecated (now removed) - push triggers removed, manual dispatch only. Will be deleted once in-flight runs complete.
|
||||
- The full pipeline (build → sign → release → changelog) now chains automatically via `workflow_run` on every push to `main`.
|
||||
- GPG signing uses `--pinentry-mode loopback` and `--passphrase-fd 0` to avoid interactive prompts on headless runners.
|
||||
- VirusTotal scans moved to the release stage so they run once per release, not once per build.
|
||||
- `.gitignore` updated to track `.b2` per-file hash files alongside existing `.sha256` and `.sig` entries.
|
||||
- Stale information removed from the guide; deprecated ODT section in Appendix A6 commented out.
|
||||
- Footer copyright information corrected.
|
||||
|
||||
!!! bug "Fixed"
|
||||
|
||||
- `_save_images_as_pdf` in `convert.py` was passing raw PNG files to `qpdf --pages`, which only accepts PDF inputs. Fixed by quantizing each page to palette mode (256 colours, FASTOCTREE) and saving as a single-page PDF before merging.
|
||||
- `convert.py` now fails immediately with install instructions if `pdftoppm` or `qpdf` are missing, instead of crashing with an unhelpful `FileNotFoundError`.
|
||||
- Pillow `KeyError: 'JPEG'` on CI resolved by installing `mkdocs-material[imaging]` and using palette-mode PDF encoding instead of RGB+JPEG.
|
||||
- Orphaned footnote citations `[^536]` and `[^537]` (Australian privacy law and the Identify and Disrupt Act) restored at the key disclosure law paragraph in the guide.
|
||||
- Broken internal links and mismatched cross-references throughout the guide corrected.
|
||||
|
||||
---
|
||||
|
||||
## [v1.2.1]
|
||||
|
||||
First automated PDF build and the start of the CI pipeline.
|
||||
|
||||
???+ tip "Added"
|
||||
|
||||
- `scripts/build_guide_pdf.py`: builds the MkDocs site and renders the full guide to a single PDF via headless Chromium (Chrome or Edge). Supports `--dark`, `--light`, and `--both` modes.
|
||||
- GitHub Actions workflow that installs Chromium, runs the build script, and uploads `export/thgtoa.pdf` as an artifact on every push to `main` or manual dispatch.
|
||||
- `docs/stylesheets/extra.css` for shared site styling.
|
||||
- This changelog.
|
||||
|
||||
!!! warning "Changed"
|
||||
|
||||
- `README.md` updated with instructions for local PDF export and a note about the GitHub Actions artifact.
|
||||
- `.gitignore` updated to exclude local build outputs (`export/`, `site/`, `_site_test/`).
|
||||
|
||||
!!! bug "Fixed"
|
||||
|
||||
- Broken reference-style internal links throughout `docs/guide/index.md` replaced with correct fragment links.
|
||||
- Broken footnote marker on the "free (unallocated) space" list item in the guide.
|
||||
|
||||
---
|
||||
|
||||
[v1.2.5]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.5
|
||||
[v1.2.4]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.4
|
||||
[v1.2.3]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.3
|
||||
[v1.2.1]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.1
|
||||
+349
-10
@@ -1,6 +1,58 @@
|
||||
---
|
||||
title: Content Contributions
|
||||
title: "Content Contributions"
|
||||
description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://anonymousplanet.net/code/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
---
|
||||
|
||||
|
||||
Install these before anything else.
|
||||
|
||||
=== "Linux / macOS"
|
||||
|
||||
```sh
|
||||
# Python 3.11+
|
||||
python3 --version
|
||||
|
||||
# poppler (pdftoppm) and qpdf
|
||||
sudo apt install poppler-utils qpdf # Debian/Ubuntu
|
||||
brew install poppler qpdf # macOS
|
||||
|
||||
# GPG
|
||||
sudo apt install gnupg # Debian/Ubuntu
|
||||
brew install gnupg # macOS
|
||||
|
||||
# Python dependencies
|
||||
pip install "mkdocs-material[imaging]" pillow numpy
|
||||
```
|
||||
|
||||
=== "Windows"
|
||||
|
||||
```powershell
|
||||
# Python 3.11+ from https://python.org
|
||||
|
||||
# poppler: download from https://github.com/oschwartz10612/poppler-windows/releases
|
||||
# Extract and add the bin\ folder to PATH
|
||||
|
||||
# qpdf: download from https://github.com/qpdf/qpdf/releases
|
||||
# Extract and add the bin\ folder to PATH
|
||||
|
||||
# GPG: download Gpg4win from https://gpg4win.org
|
||||
|
||||
# Python dependencies
|
||||
pip install "mkdocs-material[imaging]" pillow numpy
|
||||
```
|
||||
|
||||
You also need **Google Chrome** or **Microsoft Edge** installed for the light-mode PDF build (headless Chromium).
|
||||
|
||||
You can [submit bugs and feature requests](https://github.com/Anon-Planet/thgtoa/issues/new) with detailed information about your issue or idea:
|
||||
|
||||
- If you'd like to propose an addition, please follow the standards outlined here.
|
||||
@@ -10,8 +62,6 @@ You can [submit bugs and feature requests](https://github.com/Anon-Planet/thgtoa
|
||||
|
||||
For those of you who are looking to add content to the guide, include the following:
|
||||
|
||||
##### <u>Pull Requests</u>
|
||||
|
||||
- **Do** create a [topic branch] to work on instead of working directly on `main`. This helps to:
|
||||
+ Protect the process.
|
||||
+ Ensures users are aware of commits on the branch being considered for merge.
|
||||
@@ -26,7 +76,7 @@ For those of you who are looking to add content to the guide, include the follow
|
||||
- **Don't** abandon your pull request. Being responsive helps us land your changes faster.
|
||||
- **Don't** post questions in older closed PRs.
|
||||
- **Do** stick to the guide to find common style issues.
|
||||
- **Don't** make mass changes (such as replacing "I" with "we") using automated serach/replace functionality.
|
||||
- **Don't** make mass changes (such as replacing "I" with "we") using automated search/replace functionality.
|
||||
+ Search/replace doesn't understand context, and as such, will inevitably cause inconsistencies and make the guide harder to read.
|
||||
+ If it's part of a larger PR, it'll also make the reviewer's life harder, as they'll have to go through manually and undo everything by hand.
|
||||
+ _If you're going to make mass changes, take the time to do it properly_. Otherwise we'll just have to undo it anyway.
|
||||
@@ -39,23 +89,312 @@ When reporting guide issues:
|
||||
- **Don't** file duplicate reports; search for your bug before filing a new report.
|
||||
- **Don't** attempt to report issues on a closed PR.
|
||||
|
||||
### Large PRs
|
||||
|
||||
Please split large sets of changes into multiple PRs. For example, a PR that adds Windows 11 support, removes Windows AME references, and fixes typos can be split into 3 PRs. This makes PRs easier to review prior to merging.
|
||||
|
||||
For an example of what _not_ to do, see: <https://github.com/Anon-Planet/thgtoa/pull/51>. This PR contains enough changes to split into multiple smaller and individually reviewable PRs.
|
||||
|
||||
### Updating PRs
|
||||
|
||||
While a PR is being reviewed, modifications may be made to it by the reviewer prior to merging. If this is the case, a new branch will be created for the PR's review. If you would like to submit a change to a PR that is in the process of being reviewed, _do not update the PR directly_. This will only cause merge conflicts and delay the PR from being merged. Instead, submit your changes to the PR's review branch.
|
||||
|
||||
For an example of what _not_ to do, see: <https://github.com/Anon-Planet/thgtoa/pull/51>. Instead of submitting changes to the PR directly, they should have been submitted as changes to the PR's associated review branch.
|
||||
|
||||
---
|
||||
|
||||
**Thank you** for taking the few moments to read this far! You're already way ahead of the
|
||||
curve, so keep it up!
|
||||
|
||||
## Repository layout
|
||||
|
||||
```txt
|
||||
.github/
|
||||
workflows/
|
||||
01-build.yml # builds PDFs, uploads artifact
|
||||
02-sign.yml # hashes + GPG signs, uploads signatures artifact
|
||||
03-release.yml # publishes GitHub Release with all assets
|
||||
04-changelog.yml # prepends a new entry to docs/changelog/index.md
|
||||
publish.yml # deploys MkDocs site to GitHub Pages
|
||||
docs/
|
||||
guide/index.md # the guide (single Markdown file)
|
||||
changelog/ # release notes
|
||||
code/ # this page
|
||||
export/ # PDF output (PDFs gitignored; .sha256, .b2sum, .asc tracked)
|
||||
pgp/ # public signing keys
|
||||
scripts/
|
||||
build_guide_pdf.py # MkDocs + Chromium PDF builder
|
||||
convert.py # pixel-based dark mode PDF converter
|
||||
update_changelog.py # auto-generates changelog entries from git log
|
||||
setup_workflow.py # GitHub Secrets setup assistant
|
||||
verify_pdf.py # signature verification helper
|
||||
archived/
|
||||
tag_release.py # ARCHIVED - GPG tag helper (not used in current flow)
|
||||
```
|
||||
|
||||
## Building locally
|
||||
|
||||
```sh
|
||||
python scripts/build_guide_pdf.py --both
|
||||
```
|
||||
|
||||
This builds the MkDocs site, renders it to `export/thgtoa.pdf` via headless Chromium, then calls `scripts/convert.py` to produce `export/thgtoa-dark.pdf`.
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--both` | Light PDF then dark PDF |
|
||||
| (no flag) | Light PDF only |
|
||||
| `--dark` | Dark PDF only (light PDF must already exist) |
|
||||
|
||||
Build only the dark PDF from an existing light PDF:
|
||||
|
||||
```sh
|
||||
python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--dpi` | `200` | Rasterization DPI. 150 = smaller file, 300 = sharper but slow |
|
||||
| `--batch-size` | `50` | Pages per batch. Reduce if you hit OOM |
|
||||
| `--bg` | `1f1f31` | Background colour (hex) |
|
||||
| `--text` | `e0e0e0` | Body text colour (hex) |
|
||||
| `--link` | `5e8bde` | Link / blue element colour (hex) |
|
||||
|
||||
# Preview the MkDocs site
|
||||
|
||||
```sh
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
Opens at `http://127.0.0.1:8000`.
|
||||
|
||||
# CI/CD pipeline overview
|
||||
|
||||
The pipeline is fully manual after the initial build - no step automatically triggers the next. This prevents version mismatches between what was built, what was signed, and what gets released. The workflows are numbered to help guide you.
|
||||
|
||||
```txt
|
||||
push to main (or manual trigger)
|
||||
│
|
||||
▼
|
||||
01-build.yml
|
||||
Builds thgtoa.pdf + thgtoa-dark.pdf.
|
||||
Uploads artifact: pdfs
|
||||
Note the run ID.
|
||||
│
|
||||
│ # manually trigger 02-sign.yml with the build run ID
|
||||
▼
|
||||
02-sign.yml
|
||||
Downloads pdfs artifact. Hashes (SHA-256 + BLAKE2b) and GPG-signs
|
||||
all files. Commits export/ back to main. Uploads artifacts:
|
||||
signatures, pdfs-signed
|
||||
Note the run ID.
|
||||
│
|
||||
│ # manually trigger 03-release.yml with the sign run ID
|
||||
▼
|
||||
03-release.yml
|
||||
Downloads signatures + pdfs-signed artifacts. Runs VirusTotal.
|
||||
Creates GitHub Release tagged release-YYYYMMDD-<short-sha>.
|
||||
│
|
||||
│ # manually trigger 04-changelog.yml with the version string
|
||||
▼
|
||||
04-changelog.yml
|
||||
Runs update_changelog.py, prepends a new ## [vX.Y.Z] entry,
|
||||
commits back to main.
|
||||
```
|
||||
|
||||
Each stage is independent. If signing fails (e.g. an expired/revoked key, other problems in CI), re-run only `02-sign.yml` pointing at the existing build artifact - no need to rebuild the PDFs.
|
||||
|
||||
!!! warning "Before you push"
|
||||
|
||||
- Make sure the working tree is clean (`git status`)
|
||||
- Run `mkdocs build` locally if you changed `docs/` to catch broken links before CI does
|
||||
- If you added new footnotes, verify they have both a definition `[^N]:` and at least one inline citation `[^N]`
|
||||
|
||||
---
|
||||
|
||||
# Release process
|
||||
|
||||
## Trigger a build
|
||||
|
||||
Push to `main` - `01-build.yml` runs automatically when `docs/`, `mkdocs.yml`, or `scripts/` change. You can also trigger it manually from **Actions → Build PDFs → Run workflow**.
|
||||
|
||||
Once it completes successfully, **note the run ID** from the URL or the Actions list.
|
||||
|
||||
---
|
||||
|
||||
## Sign the PDFs
|
||||
|
||||
Go to **Actions → Sign PDFs → Run workflow**.
|
||||
|
||||
| Input | Value |
|
||||
|-------|-------|
|
||||
| `build_run_id` | The run ID from step 1 |
|
||||
|
||||
`02-sign.yml` will:
|
||||
|
||||
- Download the PDFs artifact from the build run
|
||||
- Compute SHA-256 and BLAKE2b hashes, writing `thgtoa.pdf.sha256`, `thgtoa.pdf.b2sum`, `sha256sums.txt`, `b2sums.txt`, and the dark equivalents
|
||||
- GPG-sign all PDFs and hash files, writing `.asc` detached signature files
|
||||
- Commit the updated `export/` directory back to `main`
|
||||
- Upload two artifacts: `signatures` and `pdfs-signed`
|
||||
|
||||
Once it completes successfully, **note the run ID**.
|
||||
|
||||
---
|
||||
|
||||
## Publish the release
|
||||
|
||||
Go to **Actions → Release → Run workflow**.
|
||||
|
||||
| Input | Value |
|
||||
|-------|-------|
|
||||
| `sign_run_id` | The run ID from step 2 |
|
||||
| `prerelease` | `false` for a normal release |
|
||||
|
||||
`03-release.yml` will:
|
||||
|
||||
- Download `signatures` and `pdfs-signed` artifacts from the sign run
|
||||
- Upload both PDFs to VirusTotal
|
||||
- Auto-generate a release tag in the format `release-YYYYMMDD-<short-sha>` (e.g. `release-20260527-abc1234`)
|
||||
- Create a GitHub Release with all PDFs, hash files, and signatures attached, and the VirusTotal report URLs in the body
|
||||
|
||||
No version number needs to be chosen at this step - the tag is derived from the date and commit SHA, so it is always unique and always traceable.
|
||||
|
||||
---
|
||||
|
||||
## Update the changelog
|
||||
|
||||
Go to **Actions → Update Changelog → Run workflow**.
|
||||
|
||||
| Input | Value |
|
||||
|-------|-------|
|
||||
| `version` | The human-readable version string, e.g. `v1.2.4` |
|
||||
| `dry_run` | `true` to preview without committing |
|
||||
|
||||
`04-changelog.yml` runs `scripts/update_changelog.py`, which:
|
||||
|
||||
- Reads git log since the last `## [vX.Y.Z]` heading in the changelog
|
||||
- Categorises commits into Added / Changed / Fixed using conventional-commit prefixes
|
||||
- Prepends a new `## [version]` admonition block to `docs/changelog/index.md`
|
||||
- Commits the result back to `main`
|
||||
|
||||
The version string is the only human decision in the release process. It goes into the changelog only - it does not affect the release tag.
|
||||
|
||||
!!! tip "Previewing the changelog entry"
|
||||
Run with `dry_run: true` first to review the generated entry before it is committed.
|
||||
|
||||
---
|
||||
|
||||
## Release tag format
|
||||
|
||||
Release tags use the format `release-YYYYMMDD-<short-sha>`, for example:
|
||||
|
||||
```txt
|
||||
release-20260527-abc1234
|
||||
```
|
||||
|
||||
This format is always unique, requires no version decision at release time, and is directly traceable to the commit that was built. The version string (e.g. `v1.2.4`) is a separate, human-assigned label that lives only in the changelog.
|
||||
|
||||
---
|
||||
|
||||
## Commit message format
|
||||
|
||||
All commits must follow the [Conventional Commits](https://www.conventionalcommits.org) format. This is enforced by the `commitizen` pre-commit hook. Not because we want to limit cooperation with others, but becasue it promotes a cleaner Changelog; we can avoid all the noise by doing this programatically.
|
||||
|
||||
```txt
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
Accepted types and their changelog bucket:
|
||||
|
||||
| Type | Bucket |
|
||||
|------|--------|
|
||||
| `feat`, `feature`, `add` | Added |
|
||||
| `fix`, `bugfix`, `revert`, `security` | Fixed |
|
||||
| `perf`, `refactor`, `change`, `chore`, `ci`, `docs`, `style`, `test`, `build` | Changed |
|
||||
|
||||
Examples:
|
||||
|
||||
```sh
|
||||
feat: add dark-mode PDF export
|
||||
fix(scripts): handle locked PDF on Windows
|
||||
docs: update developer workflow guide
|
||||
chore(ci): pin Chrome version to 120
|
||||
```
|
||||
|
||||
# Verifying a release
|
||||
|
||||
Anyone can verify the authenticity of a release download.
|
||||
|
||||
```sh
|
||||
# Import the release signing key
|
||||
gpg --import pgp/anonymousplanet-release.asc
|
||||
|
||||
# Verify the PDFs
|
||||
gpg --verify thgtoa.pdf.asc thgtoa.pdf
|
||||
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
|
||||
|
||||
# Verify the hash files
|
||||
gpg --verify sha256sums.txt.asc sha256sums.txt
|
||||
gpg --verify b2sums.txt.asc b2sums.txt
|
||||
|
||||
# Check the PDF hashes match
|
||||
sha256sum -c sha256sums.txt
|
||||
b2sum -c b2sums.txt
|
||||
```
|
||||
|
||||
A successful verify looks like:
|
||||
|
||||
```txt
|
||||
gpg: Signature made Sun 31 May 2026 03:23:26 AM EDT
|
||||
gpg: using EDDSA key C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2
|
||||
gpg: Good signature from "Anonymous Planet Release Signing Key" [ultimate]
|
||||
Primary key fingerprint: C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2
|
||||
```
|
||||
|
||||
You can safely ignore Github, Codeberg, etc. warnings like "The email in this signature doesn’t match the committer email."
|
||||
|
||||
```txt
|
||||
λ > git tag -v v1.2.3
|
||||
object cdc54d8b3bc2b286827b23921d8d4062f85295cf
|
||||
type commit
|
||||
tag v1.2.3
|
||||
tagger nopeitsnothing <no@anonymousplanet.net> 1780212206 -0400
|
||||
|
||||
v1.2.3
|
||||
gpg: Signature made Sun 31 May 2026 03:23:26 AM EDT
|
||||
gpg: using EDDSA key C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2
|
||||
gpg: Good signature from "Anonymous Planet Release Signing Key" [ultimate]
|
||||
Primary key fingerprint: C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**`cairosvg` missing during MkDocs build**
|
||||
Install the imaging extras: `pip install "mkdocs-material[imaging]"`. Required by the `social` plugin.
|
||||
|
||||
**`KeyError: 'JPEG'` in convert.py**
|
||||
Pillow needs libjpeg. Reinstall after installing the system lib: `sudo apt install libjpeg-dev && pip install --force-reinstall pillow`.
|
||||
|
||||
**`qpdf: can't find PDF header`**
|
||||
Ensure you are on the current version of `convert.py` - qpdf only accepts PDF inputs, not PNG.
|
||||
|
||||
**GPG signing fails on CI with `No secret key`**
|
||||
The `GPG_PRIVATE_KEY` secret is missing or malformed. Re-export with `gpg --armor --export-secret-keys <fingerprint>` and paste the full block including header and footer lines.
|
||||
|
||||
**GPG signing fails with `Bad passphrase`**
|
||||
The `GPG_PASSPHRASE` secret has a trailing space or newline. Paste it again with no surrounding whitespace.
|
||||
|
||||
**`03-release.yml` fails on VirusTotal**
|
||||
The `VT_API_KEY` is missing, invalid, or over the rate limit (500 requests/day on the free tier). Check the secret and re-run after a few minutes.
|
||||
|
||||
**`02-sign.yml` fails downloading PDF artifact**
|
||||
The `build_run_id` is wrong, or the artifact has expired (90-day retention). Trigger a new build and use the fresh run ID.
|
||||
|
||||
**Changelog already contains version X**
|
||||
`update_changelog.py` will error if `MANUAL_VERSION` is set to a version already in the changelog. Choose the next version string.
|
||||
|
||||
**Footnote warnings from MkDocs (`link '#fnref:N' has no anchor`)**
|
||||
A footnote definition `[^N]:` exists without a matching inline citation. Add the citation or remove the orphaned definition.
|
||||
|
||||
[discussions]: https://github.com/Anon-Planet/thgtoa/discussions
|
||||
[issues]: https://github.com/Anon-Planet/thgtoa/issues
|
||||
[help fellow users with open issues]: https://github.com/Anon-Planet/thgtoa/issues
|
||||
|
||||
+94
-87
@@ -1,87 +1,94 @@
|
||||
---
|
||||
title: Impressum
|
||||
---
|
||||
# A Constitution for an Anonymous Planet.
|
||||
|
||||
To amend the rules and regulations of the network and of the PSA community, this constitution is hereby set forth. It is applicable to all the projects of the initiative, especially the Hitchhiker's Guide to Online Anonymity. All members/collaborators must abide by these lines when contributing within the context of the initiative.
|
||||
|
||||
## Requirements
|
||||
|
||||
> Content is licensed under **[Creative Commons Attribution NonCommercial](https://creativecommons.org/licenses/by-nc/3.0/)** to prevent commercial usage.
|
||||
|
||||
### Anonymity above everything.
|
||||
Anonymity is necessary to maintain the balance of power, specifically to help journalists, whistleblowers, lawyers, scientists, and victims of oppression. Anonymity first, even if that means using non-free and/or proprietary means. Security and privacy are second, again, even if using non-free or non-open-source and/or proprietary means. In this sense, the ends may at times justify proprietary means.
|
||||
|
||||
### Independence.
|
||||
The Anonymous Planet initiative has no affiliation with the "Anonymous" collective and does not endorse their activities.
|
||||
Any overlap of their activities and our guide are purely coincidental.
|
||||
|
||||
### Accessibility.
|
||||
We will strive to always keep available the following methods of reading the Hitchhiker's Guide:
|
||||
|
||||
- online;
|
||||
- offline (e.g., PDF, ODT);
|
||||
- via the Tor network
|
||||
|
||||
### Freedom.
|
||||
Maintain free, open-source, and non-commercial nature of all our projects. This does not mean proprietary and/or closed-source tools won't be recommendeded. All scientific knowledge should be free for anyone and we support and encourage Sci-Hub and LibGen. Any attempt to erode the freedom of information and flow of knowledge of our projects, in any manner, is hostile.
|
||||
|
||||
### Verifiability, falsifiability and reproducibility.
|
||||
We will make every effort to be transparent about any and all bias we have.
|
||||
Anyone claiming to be unbiased is lying, therefore we will not falsely claim to be.
|
||||
|
||||
All our content shall be verifiable, reproducible and fact-checked:
|
||||
|
||||
- academic references (e.g., studies, papers, and peer reviewed publications);
|
||||
- reputable media references (e.g., articles, videos, and documentaries);
|
||||
- official documentation (e.g., manuals, field guides, and technical documents);
|
||||
- renowned and reputable expert review;
|
||||
- direct testing by our own collaborators for falsifiablity
|
||||
|
||||
### Innocence.
|
||||
Suspected offenders are innocent until proven guilty, with zero tolerance for abuse of power or position.
|
||||
|
||||
Any accusing/moderating member is:
|
||||
|
||||
- Subject to the burden of proving the wrong-doing of the offender.
|
||||
- Required to motivate any sanction.
|
||||
|
||||
Any offender has the right to:
|
||||
|
||||
- Face their accuser (know who is accusing them).
|
||||
- Appeal sanctions to an uninvolved third party.
|
||||
- Participate in their own incrimination (the burden of proof lies with the accuser).
|
||||
- Due process of the above.
|
||||
|
||||
### Freedom of thought.
|
||||
Open-minded and pragmatic - with no tolerance for gatekeeping.
|
||||
|
||||
Critical thinking and fact-checking are strongly encouraged; we welcome criticism including of a harsh nature (excluding ad-hominem and slurs).
|
||||
|
||||
### We do not tolerate intolerance.
|
||||
See the [Paradox of Tolerance](https://en.wikipedia.org/wiki/Paradox_of_tolerance), which includes hate speech.
|
||||
|
||||
### No analytics.
|
||||
Note that, while we will never use analytics, the (now free) platforms hosting our content might be gathering such analytics outside of our control, such as Github pages. As the initiative progresses, we will strive to avoid these as soon as possible.
|
||||
|
||||
### No profit.
|
||||
Any excess donations will only be used to support our main projects first and possibly support other intitiatives (like hosting Tor exit nodes). In all cases, we abide by the following principles:
|
||||
|
||||
- Funding transparency (i.e., all donations, spendings, source code, and future goals will be public).
|
||||
- Acceptance of donations from any entity anonymously or acknowledged (opt-in) will not have any influence on our content.
|
||||
- No sponsored content.
|
||||
- No affiliate links.
|
||||
- No product placements.
|
||||
- No advertising.
|
||||
|
||||
**Disclaimer: it is possible that, coincidentally, a donation could correlate with a recommendation. It will then be clearly stated that while the donation was welcome, the donating entity will not be gaining visibility/coverage/endorsement/recommendations due to such a donation.**
|
||||
|
||||
## Core Goals.
|
||||
|
||||
Help people in need of anonymity to maintain both their physical and digital safety.
|
||||
|
||||
## Non-Goals.
|
||||
|
||||
Help any people who are using this knowledge for bad purposes. Helping people takes precedence and we know our content can be used nefariously. Our initiative believes in having one good person given an anonymous voice, safely, is worth the risk of having several using our content for evil. As we do adhere to a fair "rule of law" system which, having 9 criminals and 1 innocent person free, is much better than having one innocent person in prison among 9 criminals.
|
||||
|
||||
**Yours faithfully, Anonymous Planet**
|
||||
---
|
||||
title: Impressum
|
||||
---
|
||||
|
||||
To amend the rules and regulations of the network and of the PSA community, this constitution is hereby set forth. It is applicable to all the projects of the initiative, especially the Hitchhiker's Guide to Online Anonymity. All members/collaborators must abide by these lines when contributing within the context of the initiative.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Anonymity above everything
|
||||
|
||||
Anonymity is necessary to maintain the balance of power, specifically to help journalists, whistleblowers, lawyers, scientists, and victims of oppression. Anonymity first, even if that means using non-free and/or proprietary means. Security and privacy are second, again, even if using non-free or non-open-source and/or proprietary means. In this sense, the ends may at times justify proprietary means.
|
||||
|
||||
### Independence
|
||||
|
||||
The Anonymous Planet initiative has no affiliation with the "Anonymous" collective and does not endorse their activities
|
||||
Any overlap of their activities and our guide are purely coincidental.
|
||||
|
||||
### Accessibility
|
||||
|
||||
We will strive to always keep available the following methods of reading the Hitchhiker's Guide:
|
||||
|
||||
- online;
|
||||
- offline (e.g., PDF, ODT);
|
||||
- via the Tor network
|
||||
|
||||
### Freedom
|
||||
|
||||
Maintain free, open-source, and non-commercial nature of all our projects. This does not mean proprietary and/or closed-source tools won't be recommendeded. All scientific knowledge should be free for anyone and we support and encourage Sci-Hub and LibGen. Any attempt to erode the freedom of information and flow of knowledge of our projects, in any manner, is hostile.
|
||||
|
||||
### Verifiability, falsifiability and reproducibility
|
||||
|
||||
We will make every effort to be transparent about any and all bias we have.
|
||||
Anyone claiming to be unbiased is lying, therefore we will not falsely claim to be.
|
||||
|
||||
All our content shall be verifiable, reproducible and fact-checked:
|
||||
|
||||
- academic references (e.g., studies, papers, and peer reviewed publications);
|
||||
- reputable media references (e.g., articles, videos, and documentaries);
|
||||
- official documentation (e.g., manuals, field guides, and technical documents);
|
||||
- renowned and reputable expert review;
|
||||
- direct testing by our own collaborators for falsifiablity
|
||||
|
||||
### Innocence
|
||||
|
||||
Suspected offenders are innocent until proven guilty, with zero tolerance for abuse of power or position.
|
||||
|
||||
Any accusing/moderating member is:
|
||||
|
||||
- Subject to the burden of proving the wrong-doing of the offender
|
||||
- Required to motivate any sanction
|
||||
|
||||
Any offender has the right to:
|
||||
|
||||
- Face their accuser (know who is accusing them)
|
||||
- Appeal sanctions to an uninvolved third party
|
||||
- Participate in their own incrimination (the burden of proof lies with the accuser)
|
||||
- Due process of the above
|
||||
|
||||
### Freedom of thought
|
||||
|
||||
Open-minded and pragmatic - with no tolerance for gatekeeping.
|
||||
|
||||
Critical thinking and fact-checking are strongly encouraged; we welcome criticism including of a harsh nature (excluding ad-hominem and slurs).
|
||||
|
||||
### We do not tolerate intolerance
|
||||
|
||||
See the [Paradox of Tolerance](https://en.wikipedia.org/wiki/Paradox_of_tolerance), which includes hate speech.
|
||||
|
||||
### No analytics
|
||||
|
||||
Note that, while we will never use analytics, the (now free) platforms hosting our content might be gathering such analytics outside of our control, such as Github pages. As the initiative progresses, we will strive to avoid these as soon as possible.
|
||||
|
||||
### No profit
|
||||
|
||||
Any excess donations will only be used to support our main projects first and possibly support other intitiatives (like hosting Tor exit nodes). In all cases, we abide by the following principles:
|
||||
|
||||
- Funding transparency (i.e., all donations, spendings, source code, and future goals will be public)
|
||||
- Acceptance of donations from any entity anonymously or acknowledged (opt-in) will not have any influence on our content
|
||||
- No sponsored content
|
||||
- No affiliate links
|
||||
- No product placements
|
||||
- No advertising
|
||||
|
||||
**Disclaimer: it is possible that, coincidentally, a donation could correlate with a recommendation. It will then be clearly stated that while the donation was welcome, the donating entity will not be gaining visibility/coverage/endorsement/recommendations due to such a donation.**
|
||||
|
||||
## Core Goals
|
||||
|
||||
Help people in need of anonymity to maintain both their physical and digital safety.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Help any people who are using this knowledge for bad purposes. Helping people takes precedence and we know our content can be used nefariously. Our initiative believes in having one good person given an anonymous voice, safely, is worth the risk of having several using our content for evil. As we do adhere to a fair "rule of law" system which, having 9 criminals and 1 innocent person free, is much better than having one innocent person in prison among 9 criminals.
|
||||
|
||||
**Yours faithfully, Anonymous Planet**
|
||||
|
||||
+112
-111
@@ -1,111 +1,112 @@
|
||||
---
|
||||
title: How to Get Involved
|
||||
---
|
||||
There are multiple ways you can add to the guide. Donations to support this project are welcome but are entirely optional. Those donations are mainly used to pay for Tor onion hosting (VPS), mail hosting, domain name registration, and to maintain/run Tor exit nodes. **No profit is ever being made**. All donations and spendings are being logged here below for transparency. Some costs for load balancer servers have been omitted for privacy reasons, but are not paid for with existing Anonymous Planet finances.
|
||||
|
||||
<span style="color: red">**Current project donation goals:**</span>
|
||||
|
||||
- <del>Funding for a VPS for hosting our .onion website</del>: **done**
|
||||
- <del>Funding for extending our domain name</del>: **Recovery of original domain secured until 2029**
|
||||
- Funding for a decent mail hosting
|
||||
- Funding for a VPS for hosting various services
|
||||
|
||||
## Donate using Monero (XMR)
|
||||
|
||||
Total Monero donations received: **7.101317184263 XMR**
|
||||
Total Monero remaining: **2.059336719397 XMR**
|
||||
|
||||
Here is the address for the main project:
|
||||
|
||||
```46crzj54eL493BA68pPT4A1MZyKQxrpZu9tVNsfsoa5nT85QqCt8cDTfy1fcTH1oyjdtUbhmpZ4QcVtfEXB337Ng6PS21ML```
|
||||
|
||||
![][1]
|
||||
|
||||
## Donate using Bitcoin (BTC)
|
||||
|
||||
Total Bitcoin donations received: **1.89353 mBTC**
|
||||
Total Bitcoin remaining: **0 mBTC**
|
||||
|
||||
Here are the addresses for the main project:
|
||||
|
||||
SegWit address: ```bc1qp9g2c6dquh5lnvft50esxsl97kupdpyqyd4kkv```
|
||||
Legacy address: ```1BBgBSVe6w4DWq2BewUQhDEjsNovhfPswD```
|
||||
|
||||
![][2]_____________________![][3]
|
||||
|
||||
## Content Contributions
|
||||
|
||||
You can easily contribute code or information suggestions at our code repositories listed at the bottom of the website and on the [Mirrors](../mirrors/index.md) tab above. We have many options that are easily accessible. Please follow our [contributing guidelines](../code/index.md) and use good PR syntax.
|
||||
|
||||
**Thank you for any contribution. All donations will be mentioned on this page.**
|
||||
|
||||
### Donations log
|
||||
|
||||
- 2021-02-06 16:48: 0.1 XMR
|
||||
- 2021-03-15 00:09: 1.24869 mBTC
|
||||
- 2021-03-15 08:41: 0.07896 mBTC
|
||||
- 2021-03-31 16:28: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-04-03 22:31: 0.5 XMR (Special thanks for this very generous donation)
|
||||
- 2021-05-07 06:22: 0.010433355105 XMR
|
||||
- 2021-06-16 03:05: 0.03 XMR
|
||||
- 2021-06-27 18:39: 0.05 XMR
|
||||
- 2021-07-12 07:24: 0.02 XMR
|
||||
- 2021-07-16 14:31: 0.1 mBTC
|
||||
- 2021-07-20 21:01: 0.058981 XMR
|
||||
- 2021-07-24 15:16: 0.000000000001 XMR
|
||||
- 2021-07-25 02:37: 0.000000000001 XMR
|
||||
- 2021-08-03 00:17: 0.04119191113 XMR
|
||||
- 2021-08-07 15:05: 0.206328241262 XMR
|
||||
- 2021-08-10 11:42: 0.21 mBTC
|
||||
- 2021-08-13 00:25: 0.25 XMR
|
||||
- 2021-08-14 04:58: 0.25588 mBTC
|
||||
- 2021-08-30 17:32: 0.000000000001 XMR
|
||||
- 2021-09-17 14:34: 0.018 XMR
|
||||
- 2021-10-01 06:23: 0.000000002137 XMR
|
||||
- 2021-10-02 19:16: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-10-17 15:40: 0.02 XMR
|
||||
- 2021-10-18 16:06: 0.1958 XMR
|
||||
- 2021-11-12 20:42: 0.02 XMR
|
||||
- 2021-11-14 18:28: 0.018 XMR
|
||||
- 2021-12-03 21:38: 0.10134722595 XMR
|
||||
- 2021-12-16 01:16: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-12-16 18:06: 0.017 XMR
|
||||
- 2022-01-09 17:54: 0.045918219893 XMR
|
||||
- 2022-01-15 17:35: 0.014 XMR
|
||||
- 2022-01-24 21:08: 0.010786 XMR
|
||||
- 2022-01-26 12:07: 0.010391 XMR
|
||||
- 2022-02-03 19:59: 0.013013984 XMR
|
||||
- 2022-02-18 17:27: 0.019 XMR
|
||||
- 2022-03-14 10:25: 0.0139887 XMR
|
||||
- 2022-07-30 03:51: 0.0222 XMR
|
||||
- 2022-09-28 05:13: 2 XMR
|
||||
- 2022-08-19: SimpleLogin.io Lifetime Premium
|
||||
- 2022-09-19: 0.345024603905 XMR (Special thanks to a previous maintainer)
|
||||
|
||||
#### Spendings log
|
||||
|
||||
- 2021-03-12: 0.08181086 XMR (+fees) for domain anonymousplanet.org (1 year)
|
||||
- 2021-03-16: 1.20179 mBTC (+fees) for domain anonymousplanet.org renewal (extension 3 years totalling 4 years)
|
||||
- 2021-04-01: 0.8317 XMR (+fees) for basic VPS for Tor Mirror hosting
|
||||
- <del>2021-04-05: 0.99367 mBTC (+fees +exchange from XMR to BTC) for Mail Hosting (1 year): <span style="color: red">**Lost**</span>
|
||||
- <del>2021-04-13: 0.71895 mBTC (+fees +exchange from XMR to BTC) for Mail Hosting (extension to 2 years)</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-04-25: 0.02892 mBTC (Wallet to Wallet transfer fee)
|
||||
- 2021-07-13: 0.78463 mBTC (+fees +exchange from BTC to XMR) for consolidation
|
||||
- <del>2021-07-13: 0.067261698061 XMR (+fees) for a Tor Exit Node (01) Hosting (3 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-07-15: 0.151959953047 XMR (+fees) for a Tor Exit Node (02) Hosting (6 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-08-16: 0.253331471239 XMR (+fees) for a Tor Exit Node (03) Hosting (12 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-08-18: AtomicSwap conversion from remaining mBTC (-0.56588) to XMR (+0.081904862179)
|
||||
- <del>2021-08-19: 0.0644 XMR (+fees) for Mail Hosting extension</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-09-18: 0.246971511836 XMR (+fees) for renewal 1 year of Tor Exit Node 01</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-10-04: 0.26954 XMR (+fees) for domain anonymousplanet.org extension until 2029
|
||||
- <del>2021-10-06: 0.236073464623 XMR (+fees) for a Tor Exit Node (04) Hosting (12 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-10-18: 0.01952 XMR (+fees) for testing a new VPS hosting provider (Privex.io) for one month</del>: <span style="color: red">**Ended**</span>
|
||||
- <del>2021-10-30: 0.240787814495 XMR (+fees) for a Synapse Hosting VPS (12 months) with bots to help grow the community. This is a test program that will be converted into a Tor Exit Node in case of failure</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-01-01: 0.28055816111 XMR (+fees) for renewal 1 year of Tor Exit Node 02</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-02-02: 0.966793601024 XMR (+fees) to sponsor a special project (w/ Universal Declaration of Human Rights)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-07-11: 0.503232784687 XMR (+fees) for 1984.is VPS (12 months)</del>: <span style="color: red">**Ended**</span>
|
||||
- <del>2022-09-19: 0.345024603905 XMR (+fees) for upgrading VPS RAM/Disk</del>: <span style="color: red">**Ended**</span>
|
||||
|
||||
[1]: ../media/monero.png
|
||||
[2]: ../media/bitcoin-segwit.png
|
||||
[3]: ../media/bitcoin-legacy.png
|
||||
---
|
||||
title: How to Get Involved
|
||||
---
|
||||
|
||||
There are multiple ways you can add to the guide. Donations to support this project are welcome but are entirely optional. Those donations are mainly used to pay for Tor onion hosting (VPS), mail hosting, domain name registration, and to maintain/run Tor exit nodes. **No profit is ever being made**. All donations and spendings are being logged here below for transparency. Some costs for load balancer servers have been omitted for privacy reasons, but are not paid for with existing Anonymous Planet finances.
|
||||
|
||||
<span style="color: red">**Current project donation goals:**</span>
|
||||
|
||||
- <del>Funding for a VPS for hosting our .onion website</del>: **done**
|
||||
- <del>Funding for extending our domain name</del>: **Recovery of original domain secured until 2029**
|
||||
- Funding for a decent mail hosting
|
||||
- Funding for a VPS for hosting various services
|
||||
|
||||
## Donate using Monero (XMR)
|
||||
|
||||
Total Monero donations received: **7.101317184263 XMR**
|
||||
Total Monero remaining: **2.059336719397 XMR**
|
||||
|
||||
Here is the address for the main project:
|
||||
|
||||
```46crzj54eL493BA68pPT4A1MZyKQxrpZu9tVNsfsoa5nT85QqCt8cDTfy1fcTH1oyjdtUbhmpZ4QcVtfEXB337Ng6PS21ML```
|
||||
|
||||
![][1]
|
||||
|
||||
## Donate using Bitcoin (BTC)
|
||||
|
||||
Total Bitcoin donations received: **1.89353 mBTC**
|
||||
Total Bitcoin remaining: **0 mBTC**
|
||||
|
||||
Here are the addresses for the main project:
|
||||
|
||||
SegWit address: ```bc1qp9g2c6dquh5lnvft50esxsl97kupdpyqyd4kkv```
|
||||
Legacy address: ```1BBgBSVe6w4DWq2BewUQhDEjsNovhfPswD```
|
||||
|
||||
![][2]_____________________![][3]
|
||||
|
||||
## Content Contributions
|
||||
|
||||
You can easily contribute code or information suggestions at our code repositories listed at the bottom of the website and on the [Mirrors](../mirrors/index.md) tab above. We have many options that are easily accessible. Please follow our [contributing guidelines](../code/index.md) and use good PR syntax. Be sure to go to the [developer guide](../code/index.md) first.
|
||||
|
||||
**Thank you for any contribution. All donations will be mentioned on this page.**
|
||||
|
||||
### Donations log
|
||||
|
||||
- 2021-02-06 16:48: 0.1 XMR
|
||||
- 2021-03-15 00:09: 1.24869 mBTC
|
||||
- 2021-03-15 08:41: 0.07896 mBTC
|
||||
- 2021-03-31 16:28: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-04-03 22:31: 0.5 XMR (Special thanks for this very generous donation)
|
||||
- 2021-05-07 06:22: 0.010433355105 XMR
|
||||
- 2021-06-16 03:05: 0.03 XMR
|
||||
- 2021-06-27 18:39: 0.05 XMR
|
||||
- 2021-07-12 07:24: 0.02 XMR
|
||||
- 2021-07-16 14:31: 0.1 mBTC
|
||||
- 2021-07-20 21:01: 0.058981 XMR
|
||||
- 2021-07-24 15:16: 0.000000000001 XMR
|
||||
- 2021-07-25 02:37: 0.000000000001 XMR
|
||||
- 2021-08-03 00:17: 0.04119191113 XMR
|
||||
- 2021-08-07 15:05: 0.206328241262 XMR
|
||||
- 2021-08-10 11:42: 0.21 mBTC
|
||||
- 2021-08-13 00:25: 0.25 XMR
|
||||
- 2021-08-14 04:58: 0.25588 mBTC
|
||||
- 2021-08-30 17:32: 0.000000000001 XMR
|
||||
- 2021-09-17 14:34: 0.018 XMR
|
||||
- 2021-10-01 06:23: 0.000000002137 XMR
|
||||
- 2021-10-02 19:16: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-10-17 15:40: 0.02 XMR
|
||||
- 2021-10-18 16:06: 0.1958 XMR
|
||||
- 2021-11-12 20:42: 0.02 XMR
|
||||
- 2021-11-14 18:28: 0.018 XMR
|
||||
- 2021-12-03 21:38: 0.10134722595 XMR
|
||||
- 2021-12-16 01:16: 1 XMR (Special thanks for this very generous donation)
|
||||
- 2021-12-16 18:06: 0.017 XMR
|
||||
- 2022-01-09 17:54: 0.045918219893 XMR
|
||||
- 2022-01-15 17:35: 0.014 XMR
|
||||
- 2022-01-24 21:08: 0.010786 XMR
|
||||
- 2022-01-26 12:07: 0.010391 XMR
|
||||
- 2022-02-03 19:59: 0.013013984 XMR
|
||||
- 2022-02-18 17:27: 0.019 XMR
|
||||
- 2022-03-14 10:25: 0.0139887 XMR
|
||||
- 2022-07-30 03:51: 0.0222 XMR
|
||||
- 2022-09-28 05:13: 2 XMR
|
||||
- 2022-08-19: SimpleLogin.io Lifetime Premium
|
||||
- 2022-09-19: 0.345024603905 XMR (Special thanks to a previous maintainer)
|
||||
|
||||
#### Spendings log
|
||||
|
||||
- 2021-03-12: 0.08181086 XMR (+fees) for domain anonymousplanet.net (1 year)
|
||||
- 2021-03-16: 1.20179 mBTC (+fees) for domain anonymousplanet.net renewal (extension 3 years totalling 4 years)
|
||||
- 2021-04-01: 0.8317 XMR (+fees) for basic VPS for Tor Mirror hosting
|
||||
- <del>2021-04-05: 0.99367 mBTC (+fees +exchange from XMR to BTC) for Mail Hosting (1 year): <span style="color: red">**Lost**</span>
|
||||
- <del>2021-04-13: 0.71895 mBTC (+fees +exchange from XMR to BTC) for Mail Hosting (extension to 2 years)</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-04-25: 0.02892 mBTC (Wallet to Wallet transfer fee)
|
||||
- 2021-07-13: 0.78463 mBTC (+fees +exchange from BTC to XMR) for consolidation
|
||||
- <del>2021-07-13: 0.067261698061 XMR (+fees) for a Tor Exit Node (01) Hosting (3 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-07-15: 0.151959953047 XMR (+fees) for a Tor Exit Node (02) Hosting (6 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-08-16: 0.253331471239 XMR (+fees) for a Tor Exit Node (03) Hosting (12 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-08-18: AtomicSwap conversion from remaining mBTC (-0.56588) to XMR (+0.081904862179)
|
||||
- <del>2021-08-19: 0.0644 XMR (+fees) for Mail Hosting extension</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-09-18: 0.246971511836 XMR (+fees) for renewal 1 year of Tor Exit Node 01</del>: <span style="color: red">**Lost**</span>
|
||||
- 2021-10-04: 0.26954 XMR (+fees) for domain anonymousplanet.net extension until 2029
|
||||
- <del>2021-10-06: 0.236073464623 XMR (+fees) for a Tor Exit Node (04) Hosting (12 months)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2021-10-18: 0.01952 XMR (+fees) for testing a new VPS hosting provider (Privex.io) for one month</del>: <span style="color: red">**Ended**</span>
|
||||
- <del>2021-10-30: 0.240787814495 XMR (+fees) for a Synapse Hosting VPS (12 months) with bots to help grow the community. This is a test program that will be converted into a Tor Exit Node in case of failure</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-01-01: 0.28055816111 XMR (+fees) for renewal 1 year of Tor Exit Node 02</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-02-02: 0.966793601024 XMR (+fees) to sponsor a special project (w/ Universal Declaration of Human Rights)</del>: <span style="color: red">**Lost**</span>
|
||||
- <del>2022-07-11: 0.503232784687 XMR (+fees) for 1984.is VPS (12 months)</del>: <span style="color: red">**Ended**</span>
|
||||
- <del>2022-09-19: 0.345024603905 XMR (+fees) for upgrading VPS RAM/Disk</del>: <span style="color: red">**Ended**</span>
|
||||
|
||||
[1]: ../media/monero.png
|
||||
[2]: ../media/bitcoin-segwit.png
|
||||
[3]: ../media/bitcoin-legacy.png
|
||||
|
||||
+673
-354
File diff suppressed because it is too large
Load Diff
+57
-20
@@ -1,36 +1,73 @@
|
||||
---
|
||||
title: ""
|
||||
description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
|
||||
title: "Home"
|
||||
description: "The Hitchhiker's Guide to Online Anonymity"
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://www.anonymousplanet.org/
|
||||
"@type": WebPage
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://www.anonymousplanet.org/authors/
|
||||
logo: ../media/favicon.png
|
||||
url: https://anonymousplanet.net/
|
||||
logo: media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
{ align=right }
|
||||
|
||||
**Welcome to the Hitchhiker's Guide.**
|
||||
<div style="text-align: center; font-size: 3.9rem; font-weight: 700; line-height: 1; margin: 2em 0;">
|
||||
<span>Privacy.</span><br>
|
||||
<span style="color: var(--crt-green);">Security.</span><br>
|
||||
<span>Anonymity.</span>
|
||||
</div>
|
||||
|
||||
Please share this project if you enjoy it and you think it might be useful to others.
|
||||
<div style="text-align: center; font-size: 3.0rem; font-weight: 400; line-height: 0.9; margin: 2em 0;">
|
||||
<span style="color: var(--crt-green);">No ads.</span><br>
|
||||
<span style="color: var(--crt-amber);">No affiliate links.</span><br>
|
||||
<span style="color: var(--crt-red);">No bullshit.</span>
|
||||
|
||||
Anonymous Planet is a collective of volunteers and contributors. No one person is considered more valuable than another, and no one person should be viewed as having "more impact" on Anonymous Planet.
|
||||
</div>
|
||||
|
||||
??? person "Das Kolburn"
|
||||
<div style="text-align: center; background: rgba(255, 0, 0, 0.1); border: 1px solid rgba(255, 0, 0, 0.3);
|
||||
border-left: 4px solid #ff4444; padding: 1em; max-width: 70ch; margin: 3em auto; color: var(--crt-red)">
|
||||
<span style="color: #ff6b6b;">⚠</span> <strong style="color: var(--status-error);">NOTICE:</strong> Always
|
||||
verify you are on <a href="https://anonymousplanet.net/" style="color:
|
||||
var(--status-error);">anonymousplanet.net</a><strong style="color: #ff6b6b;"> ⚠</strong>
|
||||
</div>
|
||||
|
||||
- [:simple-github: GitHub](https://github.com/NobodySpecial256 "@NobodySpecial256")
|
||||
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
|
||||
- [:simple-matrix: Personal Matrix](https://matrix.to/#/@daskolburn:thomcat.rocks "@daskolburn:thomcat.rocks"), [:simple-matrix: Org Matrix](https://matrix.to/#/@daskolburn:anonymousplanet.net "@daskolburn:anonymousplanet.net")
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5em; max-width:
|
||||
1000px; margin: 0 auto;">
|
||||
|
||||
??? person "Nope"
|
||||
<div class="quick-access-card">
|
||||
<h3 style="font-family: var(--code-font); font-size: 1.1rem; margin: 0;">Online</h3>
|
||||
<p style="margin: 0 0 1em;">Comprehensive coverage of tracking techniques, ID verification, and how to create
|
||||
truly anonymous identities.</p>
|
||||
<a href="guide/" style="font-family: var(--code-font); font-size: 0.85rem; color: var(--crt-amber);">Read online
|
||||
</a>
|
||||
</div>
|
||||
|
||||
- [:simple-github: GitHub](https://github.com/nopeitsnothing "@nopeitsnothing")
|
||||
- [:simple-mastodon: Mastodon](https://ioc.exchange/@unknown "@unknown@ioc.exchange"){rel=me}
|
||||
- [:fontawesome-solid-house: Homepage](https://www.itsnothing.net)
|
||||
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
|
||||
- [:simple-matrix: Personal Matrix](https://matrix.to/#/@thehidden:tchncs.de "@thehidden:tchncs.de"), [:simple-matrix: Org Matrix](https://matrix.to/#/@nope:anonymousplanet.net "@nope:anonymousplanet.net")
|
||||
<div class="quick-access-card">
|
||||
<h3 style="font-family: var(--code-font); font-size: 1.1rem; margin: 0;">PDF</h3>
|
||||
<p style="margin: 0 0 1em;">Download the guide as a PDF (best for readability) or ODT. Verify integrity with the
|
||||
provided public key before trusting any downloaded file.</p>
|
||||
<a href="export/thgtoa.pdf" class="btn-download" style="min-width: auto !important; color: var(--crt-amber);">Get
|
||||
the files </a>
|
||||
</div>
|
||||
|
||||
<div class="quick-access-card">
|
||||
<h3 style="font-family: var(--code-font); font-size: 1.1rem; margin: 0;">Tor</h3>
|
||||
<p style="margin: 0 0 1em;">Access the guide over the Tor network for maximum privacy. The .onion address ensures
|
||||
you reach us without exposing your IP or destination.</p>
|
||||
<a href="mirrors/" style="font-family: var(--code-font); font-size: 0.85rem; color: var(--crt-amber);">Open .onion
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; font-family: var(--code-font); font-size: 0.8rem; padding: 1.5em 0; border-top:
|
||||
1px solid rgba(255,255,255,0.1); margin-top: 4em; color: var(--crt-green); opacity: 0.9;">
|
||||
<strong>The Hitchhiker's Guide</strong> is
|
||||
licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" style="color: var(--crt-amber);">CC BY-SA
|
||||
4.0</a>.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
+9
-14
@@ -4,32 +4,27 @@ description: Maintainers of the Hitchhiker's Guide and the PSA Community.
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://www.anonymousplanet.org/
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet
|
||||
url: https://www.anonymousplanet.org/mirrors/
|
||||
logo: ../media/favicon.png
|
||||
url: https://anonymousplanet.net/mirrors/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
{ align=right }
|
||||
|
||||
---
|
||||
???+ tip "Where to find the Hitchhiker's Guide"
|
||||
|
||||
!!! Note "Where to find the Hitchhiker's Guide"
|
||||
|
||||
- [Original](https://anonymousplanet.org)
|
||||
- [Original](https://anonymousplanet.net)
|
||||
- [Tor v3](http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion) **Down**
|
||||
- [Archive.org](https://web.archive.org/web/https://anonymousplanet.org)
|
||||
- [Archive.today](https://archive.fo/anonymousplanet.org)
|
||||
- [Archive.today over Tor](http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/anonymousplanet.org)
|
||||
- [Archive.org](https://web.archive.org/web/https://anonymousplanet.net)
|
||||
- [Archive.today](https://archive.fo/anonymousplanet.net)
|
||||
- [Archive.today over Tor](http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/anonymousplanet.net)
|
||||
|
||||
!!! Note "PDF export (single file)"
|
||||
|
||||
The guide is also available as a **PDF** (images and layout preserved). It is built automatically in GitHub Actions: open [**Build guide PDF**](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) on the [**thgtoa** source repository](https://github.com/Anon-Planet/thgtoa), pick a successful run, and download the **`guide-pdf`** artifact. You can start a fresh build anytime (**Actions** → **Build guide PDF** → **Run workflow**).
|
||||
|
||||
To produce the same file locally, clone the repository and run `python scripts/build_guide_pdf.py` (Python, [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/), and **Google Chrome** or **Microsoft Edge** required). More detail is in the [repository README](https://github.com/Anon-Planet/thgtoa#ways-to-read-or-export-the-guide).
|
||||
The guide is also available as a **PDF** (images and layout preserved). It is built automatically. See the [Releases](https://github.com/Anon-Planet/thgtoa/releases). More detail is in the [repository README](https://github.com/Anon-Planet/thgtoa#ways-to-read-or-export-the-guide).
|
||||
|
||||
!!! Note "Our official git mirrors"
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEZc0QYxYJKwYBBAHaRw8BAQdAm8mOR8/0qWrm9Tqzfl9Ks5rjtIbQZLAR/qxH
|
||||
HVGJsxi0LUFub255bW91cyBQbGFuZXQgRW1haWwgRW5jcnlwdGlvbi9TaWduaW5n
|
||||
IEtleYiTBBMWCgA7FiEE/L0sq979H7ounnWRoags0t0s+JAFAmXNEGMCGwMFCwkI
|
||||
BwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQoags0t0s+JDbRAEAuZlBmMGgZ3bh
|
||||
12Js9jjDcu+jhKqL4fJrJG5z9+KFkQwA/An1StA6EhcM7qlzZ5bzm2SZAbP9hQRZ
|
||||
GmfaeU2P5KgHiHgEMBYKACAWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaiCYTgId
|
||||
AAAKCRAX7KBfdo3t9gNUAP9/SyGBYJ7s9YeqLHOJ+veQZjZYHvFGQ7yPn0Fetx0Z
|
||||
LAD/UOQ8rP2QaldCMyVSG8SqfPd7n++SEAXWAl2gAo9mhg6IdQQQFgoAHRYhBJ+l
|
||||
Q20O42CYUVc4JRfsoF92je32BQJmpEUQAAoJEBfsoF92je32tD8A/ir9hE8UjrJE
|
||||
psG+PNfxYAwAagKUGbAMDUxQp3z+t81+AP45hYT4aR89zSQaankHLs3Lh7Cp5ael
|
||||
NBe/BtfR9hCLAYkCMwQQAQgAHRYhBF7WeRgs0hkTAMDm6kyyELegkVLWBQJqIJiM
|
||||
AAoJEEyyELegkVLWjZYP/j1k5vl+r0NDQXmE8hS9IKhaQPggP72iXc5RWeMQHuIv
|
||||
b1laQZm64xerJNdAh0uk1bwfmJnVGfyxBUrlCgAIeVGRSlni2Rig4azaQ1IS0pqF
|
||||
4sC1KzKEhEaNdkh3pJyGtP1cikcSjWeU2oYQou3/7VN3vNyW+n8OAVF+2fsC5d78
|
||||
EvdpZgal+komb+J8Bt552uDbCCVI4TFIPBZmHWoXjaP6L+730YphbV7Aw0L5J6OO
|
||||
ob0nzHn4X0dIvGE7Phdp2e1yNRUOSRLh8B/D5OiE9k7CaeYmJNPv5qOw/R+NgrrA
|
||||
ZFnoOuwHo0D+aL9WT9q4aM/cDCEIbvhQ4l5ZhVGqZuQ9wxNCgPi3ZiZRTfk1PW4v
|
||||
uMw1xGwXBKy7jDO12xWIWWv9MiwIQLw0OxSxKbr76rgucq7e7JrWr64rItu5Wm7F
|
||||
8qxg2cwmDat6tFSRVWlEDy8oNkRMJNjdQJDu3ez9YOfJNnApAz94Of1XU7CUuYjY
|
||||
PV88BaHdUBVtANEzy0iSDCcSj6auzLfv9dBN8cOdUxlVcrPf2jjK6JR/6qe6VWNp
|
||||
wRg9VQW2fe8HJTMUt0o9qQBJUsF68KOHtIdoE4az9AyyBNKl67dKqLB9HoIItLzD
|
||||
MJRcbS2p6plCTNagwPVvgtPRChll9JP3jLPVhRL2BixYVkbHUoJxsEfscTUl6Azt
|
||||
tEtBbm9ueW1vdXMgUGxhbmV0IEVtYWlsIEVuY3J5cHRpb24vU2lnbmluZyBLZXkg
|
||||
PGFub255bW91c3BsYW5ldEBkaXNyb290Lm9yZz6IkwQTFgoAOxYhBPy9LKve/R+6
|
||||
Lp51kaGoLNLdLPiQBQJmhqXqAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
|
||||
AAoJEKGoLNLdLPiQJ34A/RJT9Hyj7hT/0D1BbDU6s6YzD+/x7Pyq2+9kNSI0L77W
|
||||
AQAAG+CfDrKDXJtBNKZVNFZpld3wUeoIOcAqLl7KpsVGCYh1BBAWCgAdFiEEn6VD
|
||||
bQ7jYJhRVzglF+ygX3aN7fYFAmakRRAACgkQF+ygX3aN7fbypgEAnEg0IbWnpaLj
|
||||
/4wU179vUZZu/Y0DE63GbJuZjj72hKUA/0xyzIgSvXByjoOkEwCn5w1+RPYXKw7Z
|
||||
syERsDCUAAMIiQIzBBABCAAdFiEEXtZ5GCzSGRMAwObqTLIQt6CRUtYFAmogmIwA
|
||||
CgkQTLIQt6CRUtafPQ/+LTWFU84tDZAM0Hp7bWB0dw8nP0JvNQ2WtZf0flh+r1tF
|
||||
cmVnc9szZBh+zzSpY25iK5+Waa6+l1POYSQpkS67VR0Jrv9nL94YrRhqalSRWsjW
|
||||
MQJO+Obu4LIRIqiMZLJlAd9Bg9FshYagbQDVDOI8v9mxqCzIVm3tBx1Jp57ATHgm
|
||||
sMDWn7l1BI0SkLlG49LYxVDQ6QAx4XLCQw+JzdiJs+yExa5ymYmV61evVVbDV5UF
|
||||
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
|
||||
Eztpvc4cyyGAmI42SLM/jKbqO2yWqwVoAg==
|
||||
=ww/S
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "PGP"
|
||||
description: "Import our GPG keys to verify our releases and signed content."
|
||||
schema:
|
||||
"@context": https://schema.org
|
||||
"@type": Organization
|
||||
"@id": https://anonymousplanet.net/
|
||||
name: Anonymous Planet PGP
|
||||
url: https://anonymousplanet.net/pgp/
|
||||
logo: ../media/profile.png
|
||||
sameAs:
|
||||
- https://github.com/Anon-Planet
|
||||
- https://opencollective.com/anonymousplanetorg
|
||||
- https://mastodon.social/@anonymousplanet
|
||||
---
|
||||
|
||||
# PGP
|
||||
|
||||
<div style="font-family: var(--text-primary); color: var(--crt-green); font-size: 1.1rem; margin-bottom: 2em;">
|
||||
<a href="anonymousplanet.asc" class="btn-download">
|
||||
>>Download Our Public Keyring<<
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Anonymous Planet uses GnuPG keys as the standard for encryption/signing. Import this keyring to verify the authenticity of our releases, signed content, and secure your emails to us.
|
||||
|
||||
## Anonymous Planet Keys
|
||||
|
||||
Our entire keyring is available at <https://anonymousplanet.net/pgp/anonymousplanet.asc> (click the above button to get it). It is also pasted below.
|
||||
|
||||
| Name | Key ID | Purpose |
|
||||
|------|--------|---------|
|
||||
| Master Signing Key (MSK) | `9FA5436D0EE360985157382517ECA05F768DEFDA` | Announcements, signing subkeys, etc. |
|
||||
| Release Signing Key (RSK) | `C3023DBEA3FB38C438BA1EECEC60AEDE8B992A2` | Release signing, occasional commit signing |
|
||||
| Email Encryption/Signing Key (ESK) | `FCBD2CABDEFD1FBA2E9E7591A1A82CD2DD2CF890` | Secure email |
|
||||
|
||||
**Fingerprint verification:** Always verify key fingerprints against our [GitHub announcements](https://github.com/Anon-Planet/thgtoa/releases) before importing.
|
||||
|
||||
## Key Rotation
|
||||
|
||||
We may rotate keys periodically. Check our [GitHub Releases](https://github.com/Anon-Planet/thgtoa/releases) and [changelog](../changelog/index.md) for announcements.
|
||||
|
||||
```txt
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEZc0QYxYJKwYBBAHaRw8BAQdAm8mOR8/0qWrm9Tqzfl9Ks5rjtIbQZLAR/qxH
|
||||
HVGJsxi0LUFub255bW91cyBQbGFuZXQgRW1haWwgRW5jcnlwdGlvbi9TaWduaW5n
|
||||
IEtleYiTBBMWCgA7FiEE/L0sq979H7ounnWRoags0t0s+JAFAmXNEGMCGwMFCwkI
|
||||
BwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQoags0t0s+JDbRAEAuZlBmMGgZ3bh
|
||||
12Js9jjDcu+jhKqL4fJrJG5z9+KFkQwA/An1StA6EhcM7qlzZ5bzm2SZAbP9hQRZ
|
||||
GmfaeU2P5KgHiHgEMBYKACAWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaiCYTgId
|
||||
AAAKCRAX7KBfdo3t9gNUAP9/SyGBYJ7s9YeqLHOJ+veQZjZYHvFGQ7yPn0Fetx0Z
|
||||
LAD/UOQ8rP2QaldCMyVSG8SqfPd7n++SEAXWAl2gAo9mhg6IdQQQFgoAHRYhBJ+l
|
||||
Q20O42CYUVc4JRfsoF92je32BQJmpEUQAAoJEBfsoF92je32tD8A/ir9hE8UjrJE
|
||||
psG+PNfxYAwAagKUGbAMDUxQp3z+t81+AP45hYT4aR89zSQaankHLs3Lh7Cp5ael
|
||||
NBe/BtfR9hCLAYkCMwQQAQgAHRYhBF7WeRgs0hkTAMDm6kyyELegkVLWBQJqIJiM
|
||||
AAoJEEyyELegkVLWjZYP/j1k5vl+r0NDQXmE8hS9IKhaQPggP72iXc5RWeMQHuIv
|
||||
b1laQZm64xerJNdAh0uk1bwfmJnVGfyxBUrlCgAIeVGRSlni2Rig4azaQ1IS0pqF
|
||||
4sC1KzKEhEaNdkh3pJyGtP1cikcSjWeU2oYQou3/7VN3vNyW+n8OAVF+2fsC5d78
|
||||
EvdpZgal+komb+J8Bt552uDbCCVI4TFIPBZmHWoXjaP6L+730YphbV7Aw0L5J6OO
|
||||
ob0nzHn4X0dIvGE7Phdp2e1yNRUOSRLh8B/D5OiE9k7CaeYmJNPv5qOw/R+NgrrA
|
||||
ZFnoOuwHo0D+aL9WT9q4aM/cDCEIbvhQ4l5ZhVGqZuQ9wxNCgPi3ZiZRTfk1PW4v
|
||||
uMw1xGwXBKy7jDO12xWIWWv9MiwIQLw0OxSxKbr76rgucq7e7JrWr64rItu5Wm7F
|
||||
8qxg2cwmDat6tFSRVWlEDy8oNkRMJNjdQJDu3ez9YOfJNnApAz94Of1XU7CUuYjY
|
||||
PV88BaHdUBVtANEzy0iSDCcSj6auzLfv9dBN8cOdUxlVcrPf2jjK6JR/6qe6VWNp
|
||||
wRg9VQW2fe8HJTMUt0o9qQBJUsF68KOHtIdoE4az9AyyBNKl67dKqLB9HoIItLzD
|
||||
MJRcbS2p6plCTNagwPVvgtPRChll9JP3jLPVhRL2BixYVkbHUoJxsEfscTUl6Azt
|
||||
tEtBbm9ueW1vdXMgUGxhbmV0IEVtYWlsIEVuY3J5cHRpb24vU2lnbmluZyBLZXkg
|
||||
PGFub255bW91c3BsYW5ldEBkaXNyb290Lm9yZz6IkwQTFgoAOxYhBPy9LKve/R+6
|
||||
Lp51kaGoLNLdLPiQBQJmhqXqAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
|
||||
AAoJEKGoLNLdLPiQJ34A/RJT9Hyj7hT/0D1BbDU6s6YzD+/x7Pyq2+9kNSI0L77W
|
||||
AQAAG+CfDrKDXJtBNKZVNFZpld3wUeoIOcAqLl7KpsVGCYh1BBAWCgAdFiEEn6VD
|
||||
bQ7jYJhRVzglF+ygX3aN7fYFAmakRRAACgkQF+ygX3aN7fbypgEAnEg0IbWnpaLj
|
||||
/4wU179vUZZu/Y0DE63GbJuZjj72hKUA/0xyzIgSvXByjoOkEwCn5w1+RPYXKw7Z
|
||||
syERsDCUAAMIiQIzBBABCAAdFiEEXtZ5GCzSGRMAwObqTLIQt6CRUtYFAmogmIwA
|
||||
CgkQTLIQt6CRUtafPQ/+LTWFU84tDZAM0Hp7bWB0dw8nP0JvNQ2WtZf0flh+r1tF
|
||||
cmVnc9szZBh+zzSpY25iK5+Waa6+l1POYSQpkS67VR0Jrv9nL94YrRhqalSRWsjW
|
||||
MQJO+Obu4LIRIqiMZLJlAd9Bg9FshYagbQDVDOI8v9mxqCzIVm3tBx1Jp57ATHgm
|
||||
sMDWn7l1BI0SkLlG49LYxVDQ6QAx4XLCQw+JzdiJs+yExa5ymYmV61evVVbDV5UF
|
||||
pEwW6nsuEDc68UN6npjr8OuGH5y+1ot1vaBderoXFZ8hRG/czzODX5L0zGDX9R2C
|
||||
cGyIrv4AoXTtnbiVZGG6Vn1p3C/RMFZsVOMKvyQKh0rjcD9dqVQ4thI41o92jZ0V
|
||||
K5ALjPiWe1kM4DVYgk/b46q9/8rjzYb4WJCwPQJkRBp36y26oRWM0JaY2Tobzt/H
|
||||
3c8d36hQSXtjKLY27ZY5jL0N4vJaiclAuy03wKonmKlUc1ROUBEgNoZcvx6rLx6e
|
||||
64G7ypOpvlQCcLT/3x+VqX+KTwf4bbigrlonFMpq2lX/uwvHDMfc9/yB5xaUKLpf
|
||||
/zuk/gHKzAfKPItzEyRx5Lvql9Aywaa+/gTCZhwM3D6DzR5Q5waDXcdsptB+GZAi
|
||||
5s+BTxe1a4H6PMobdNOsYDFa77QKQXtWdHkybhV5xzRRMoSdKi+zwvU77BRnwf24
|
||||
OARlzRBjEgorBgEEAZdVAQUBAQdApPitK71WFqWUCycq2bWYYykmU1YFgea3q/V3
|
||||
DfsbbhIDAQgHiHgEGBYKACAWIQT8vSyr3v0fui6edZGhqCzS3Sz4kAUCZc0QYwIb
|
||||
DAAKCRChqCzS3Sz4kLhXAQDhI8tMCEWLu3MhG9pI8BBYH4fS7kuN8ggxqDSbRpKJ
|
||||
dgEAk1CA06WvsH4/n0HmJ83sJSbmFGmEMp2RyvKbdCIW5gKYMwRlzRBIFgkrBgEE
|
||||
AdpHDwEBB0CVyNrq08EGyU77is+cf7/vqDqi95rCeZvE7yRU7SYFDrQjQW5vbnlt
|
||||
b3VzIFBsYW5ldCBNYXN0ZXIgU2lnbmluZyBLZXmIkgQTFgoAOxYhBJ+lQ20O42CY
|
||||
UVc4JRfsoF92je32BQJlzRBIAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
|
||||
AAoJEBfsoF92je32NywA+JKlENQl/Kn03FojFNC1Xw5dfNMKnDAs6lV/loSDtOYB
|
||||
ALrDCc1eWeeBt0FQItPiNcGycBBbRtJciNJMu2AUQ9wCiJMEExYKADsWIQSfpUNt
|
||||
DuNgmFFXOCUX7KBfdo3t9gUCZc0QSAIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe
|
||||
BwIXgAAKCRAX7KBfdo3t9jcsAQAAkqUQ1CX8qfTcWiMU0LVfDl180wqcMCzqVX+W
|
||||
hIO05gEAusMJzV5Z54G3QVAi0+I1wbJwEFtG0lyI0ky7YBRD3AKIdQQQFgoAHRYh
|
||||
BIs6dIkFNrrVDZN26/HLMvZ+MwKhBQJqIJYjAAoJEPHLMvZ+MwKhm7YA/Rdrap0+
|
||||
zzfVtXomRmVkeIaabzxImPuYnvwvgSulFw0oAP9ZkmMjexGKnbuLc1znUNoUjKyR
|
||||
SmpT0ezNJRPcB2x3DokCMwQQAQgAHRYhBF7WeRgs0hkTAMDm6kyyELegkVLWBQJq
|
||||
IJcQAAoJEEyyELegkVLW2YQP/0ry3BvS1pmEl60Ty0smBtEfoYsqQOz4uMBeOYzN
|
||||
IHXtFrw19XAZQjVXYRhUp9NOol6JY8KtqUg0LXQZaRWhVwbA6hMqDbFeT+l+Psu/
|
||||
Ek3dghpwR6xEDSNcm3V1aznNgADcDkGLINbZ7ZW/iDnrws5JMDA0k3+Qt1d596Le
|
||||
kv609g28bxGgt0YENUDFGwXTawO0PALMF3Xg4gwyGU8UELoCoUUWvCYEECqO1vWc
|
||||
BrZNDNulp9ovfsC8A4BkAo6yCv6RPOJVGHaKlfsO81HvBz+pExT0S71DFX5Gm9Qo
|
||||
zkDIEZKLuBji6zuhi88dm17vvDs2SKjVd9OnZhs8THbGW+4WRqU6woYMN1YJAedp
|
||||
+hAaYhJjQfdnFXql7bY5f9uqiBLGy4c5BPoXGYQNi8GABCzUdoiBwsFM/DQ9L8qA
|
||||
fA355CVayg3aODo/NGore3N2Gqxa0GUz21ImMRV/8EIR05zFRVHeR7gu2czDyGih
|
||||
9eHadE2FAAmu2iifZcxKfe3ibSBijub11Wxkfei1gipQ/OvkEfCONVVNRyi6H9Kv
|
||||
6lRP+2n93GQLxlcqxd1qW2tpAt8Pimetb0M20ZY3LkuxhXvsir3sRFRcU4dLSbld
|
||||
7VdwG7AsMmmA98Tp6CKjzI9FS/JcZTDoAVw6PgDSthrK5ev2plALMtWrOg9TggYE
|
||||
6a/nuDgEZc0QSBIKKwYBBAGXVQEFAQEHQP1nHDDQfCi8qGG2QJj/wmMUl8ZGEiAY
|
||||
pVc/+S0ZIJEnAwEIB4h4BBgWCgAgFiEEn6VDbQ7jYJhRVzglF+ygX3aN7fYFAmXN
|
||||
EEgCGwwACgkQF+ygX3aN7fbSGAD9GLAarXceWbfEUWYC4IwVJAKSHDPWSzLGgFnV
|
||||
x/D3238A/RiJHKYzmigvFLL/A28WStW6P47CjNYjJCS490qG/L0GmDMEZc0J8xYJ
|
||||
KwYBBAHaRw8BAQdAWIpOKf8GnTINRH7uW4oeGW4D4vfmK9xeQrnqn/TMIMe0JEFu
|
||||
b255bW91cyBQbGFuZXQgUmVsZWFzZSBTaWduaW5nIEtleYiTBBMWCgA7FiEEwwI9
|
||||
vqP7OMQ4uh7tzsYK7ei5kqIFAmXNCfMCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwEC
|
||||
HgcCF4AACgkQzsYK7ei5kqJJVgD+NKdW7U/uMWl6Ov1Ye9PPy6MbIyyCYd2j5snO
|
||||
60e7msQA/0rxLaeLwzraevcE+WpdPMadxP2M8MxIKrKeAkKAe+IJiHUEEBYKAB0W
|
||||
IQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCZqRFIAAKCRAX7KBfdo3t9o9LAP426yx7
|
||||
1EP9sLKKpkkdAT19HJgsNBeA7SdR/DtMzWEbegD/f2oQYwVz3O1w7xuUqJMHS6/b
|
||||
N1E8B78JSi576up9rA2IdQQQFgoAHRYhBJ+lQ20O42CYUVc4JRfsoF92je32BQJp
|
||||
508bAAoJEBfsoF92je32TM8A/2j51Jc3owAx9STceeamG5GG7inq5jRMyKlMG4Kw
|
||||
1y1lAQD2kKSR9tz/l4Yhvy96WOuQYb+uG0W78T12l2c61F/xBrg4BGXNCfMSCisG
|
||||
AQQBl1UBBQEBB0DOf/mxiZClX/sJqtj7Ob+pCHbsMp9Wd4SHW7/PFaUKHwMBCAeI
|
||||
eAQYFgoAIBYhBMMCPb6j+zjEOLoe7c7GCu3ouZKiBQJlzQnzAhsMAAoJEM7GCu3o
|
||||
uZKie1EBAL5P2th3moOj4IDdXrP6KgdBB0kYweAHix0djG1jV/1+AQDrgVyMPBbT
|
||||
Eztpvc4cyyGAmI42SLM/jKbqO2yWqwVoAg==
|
||||
=ww/S
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
@@ -0,0 +1,105 @@
|
||||
.md-typeset a::after,
|
||||
a:not(.btn)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: var(--bg-card);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.md-typeset a:hover::after,
|
||||
a:not(.btn):hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md-typeset code {
|
||||
background-color: rgba(0, 26, 3, 0.226) !important;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.md-typeset .admonition code {
|
||||
background-color: rgba(0, 26, 3, 0.226) !important;
|
||||
}
|
||||
|
||||
.admonition {
|
||||
border-left: 4px solid var(--crt-amber);
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(0, 255, 0, 0.03) 100%);
|
||||
}
|
||||
|
||||
.admonition-note,
|
||||
.admonition-todo,
|
||||
.admonition-important {
|
||||
background: linear-gradient(180deg, var(--bg-secondary) 0%, rgba(0, 255, 0, 0.05) 100%) !important;
|
||||
}
|
||||
|
||||
.admonition-tip,
|
||||
.admonition-hint,
|
||||
.admonition-important,
|
||||
.admonition-new {
|
||||
background: linear-gradient(180deg, var(--bg-secondary) 0%, rgba(0, 255, 0, 0.05) 100%) !important;
|
||||
}
|
||||
|
||||
.admonition-caution,
|
||||
.admonition-warning,
|
||||
.admonition-failure {
|
||||
background: linear-gradient(180deg, var(--bg-secondary) 0%, rgba(255, 165, 0, 0.08) 100%) !important;
|
||||
}
|
||||
|
||||
.admonition-error,
|
||||
.admonition-failure {
|
||||
background: linear-gradient(180deg, var(--bg-secondary) 0%, rgba(239, 68, 68, 0.08) 100%) !important;
|
||||
}
|
||||
|
||||
.md-typeset__table th {
|
||||
background-color: var(--bg-primary) !important;
|
||||
border-bottom: 2px solid var(--accent-green);
|
||||
}
|
||||
|
||||
.md-typeset h1,
|
||||
.md-typeset h2,
|
||||
.md-typeset h3 {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-search__overlay .md-search__scrollwrap .md-search__form {
|
||||
background-color: var(--bg-card) !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-search__overlay .md-search__scrollwrap .md-search__form input {
|
||||
background-color: var(--bg-card) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-search__overlay .md-search__results {
|
||||
background-color: var(--bg-card) !important;
|
||||
}
|
||||
|
||||
.md-footer {
|
||||
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
|
||||
border-top: 2px solid var(--accent-green);
|
||||
}
|
||||
|
||||
/* High contrast mode overrides */
|
||||
@media (prefers-contrast: high) {
|
||||
.admonition-note,
|
||||
.admonition-tip,
|
||||
.admonition-important,
|
||||
.md-typeset details.admonition {
|
||||
background: var(--bg-color) !important;
|
||||
}
|
||||
|
||||
nav[data-md-level="1"] .md-nav__link,
|
||||
nav[data-md-level="2"] .md-nav__link {
|
||||
color: var(--bg-color) !important;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/* Title sheet: visible only when printing / generating PDF (not on screen). */
|
||||
.pdf-title-page {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.pdf-title-page {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
padding: 5rem 2rem 4rem;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.pdf-title-page__title {
|
||||
font-size: 1.65rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.pdf-title-page__subtitle {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.4;
|
||||
margin: 0 0 2rem;
|
||||
}
|
||||
|
||||
.pdf-title-page__meta {
|
||||
font-size: 0.95rem;
|
||||
font-style: normal;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Guide landing: small floating logo so opening copy flows beside it (HTML + PDF). */
|
||||
.guide-intro-lead {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.guide-intro-lead > p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.guide-intro-lead > p:first-child img {
|
||||
float: right;
|
||||
max-width: 6.5rem;
|
||||
height: auto;
|
||||
margin: 0 0 0.5rem 1rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.guide-intro-lead > p:first-child img {
|
||||
max-width: 5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
#md-content > :first-child:is(h1, h2, h3),
|
||||
.md-typeset__content > :first-child:is(h1, h2, h3) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.social,
|
||||
.social__link {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.75em !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.social svg,
|
||||
.social__link svg {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
#social,
|
||||
.social-container,
|
||||
.md-social-list {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
gap: 0.75em !important;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.md-social,
|
||||
[data-md-color-scheme="slate"] .md-typeset a[href*="social"],
|
||||
.md-social__link {
|
||||
display: inline-flex !important;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.social span,
|
||||
.social__link span {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#social > :is(a[href], .md-icon),
|
||||
.social__link > :is(a[href], .md-icon) {
|
||||
display: inline-flex !important;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.md-social-list,
|
||||
.social-container {
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
|
||||
body .social,
|
||||
body .md-footer__social {
|
||||
display: inline-flex !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.social::before,
|
||||
.social::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 60em) {
|
||||
.social {
|
||||
margin-right: 0.5em !important;
|
||||
}
|
||||
|
||||
.social svg,
|
||||
.social__link svg {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
#social {
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.md-footer h4,
|
||||
.md-footer p {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.md-social {
|
||||
display: inline-flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
gap: 0.75em !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.md-social__link {
|
||||
display: inline-flex !important;
|
||||
flex-direction: row !important;
|
||||
align-items: center !important;
|
||||
white-space: nowrap !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.md-social__link span {
|
||||
font-size: 0.9rem !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.md-social__link svg {
|
||||
display: inline-flex !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.md-footer-meta__inner {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
background-color: var(--bg-color) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 60em) {
|
||||
.md-social {
|
||||
gap: 0.5em !important;
|
||||
}
|
||||
|
||||
.md-social__link svg {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
.md-social__link span {
|
||||
font-size: 0.8rem !important;
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
:root {
|
||||
--md-primary-fg-color:#4052b500;
|
||||
--bg-color: #020307;
|
||||
--text-primary: #c5c5c5;
|
||||
--text-secondary: #a0a0a0;
|
||||
--accent-color: #039634;
|
||||
--border-color: #0b85005d;
|
||||
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.no-js .md-sidebar {
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-main);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--crt-green);
|
||||
font-weight: 700;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
color: var(--crt-green);
|
||||
font-weight: 600;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--crt-green);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--bg-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 20px 0;
|
||||
filter: brightness(1.1);
|
||||
animation: none !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.card, table th {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
background-color: var(--bg-card) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
nav, footer, button {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
.command-palette {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 0.5em;
|
||||
border-top: 1px dashed var(--terminal-line-color);
|
||||
border-bottom: 1px dashed var(--terminal-line-color);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(0, 255, 0, 0.03) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.command-palette::before,
|
||||
.command-palette::after {
|
||||
content: "|";
|
||||
color: var(--accent-green);
|
||||
animation: cursorBlink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes cursorBlink {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.headerlink {
|
||||
color: var(--crt-green);
|
||||
font-size: 0.8rem;
|
||||
margin-left: 0.5em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.terminal-session {
|
||||
font-family: var(--code-font);
|
||||
text-align: center;
|
||||
padding: 0.25em 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.system-status {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.4em 1em;
|
||||
border-radius: 4px;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 0, 0.1);
|
||||
}
|
||||
|
||||
.system-status .status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--status-success);
|
||||
margin-right: 0.5em;
|
||||
box-shadow: 0 0 4px var(--status-success);
|
||||
}
|
||||
|
||||
.terminal-corner {
|
||||
position: fixed;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
border-top: 2px solid var(--accent-green);
|
||||
border-left: 2px solid var(--accent-green);
|
||||
}
|
||||
|
||||
.top-right {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
border-top: 2px solid var(--accent-green);
|
||||
border-right: 2px solid var(--accent-green);
|
||||
}
|
||||
|
||||
.bottom-left {
|
||||
bottom: 60px;
|
||||
left: 20px;
|
||||
border-bottom: 2px solid var(--accent-green);
|
||||
border-left: 2px solid var(--accent-green);
|
||||
}
|
||||
|
||||
.hud-line {
|
||||
position: fixed;
|
||||
left: 5%;
|
||||
top: 30%;
|
||||
right: 5%;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(0, 255, 0, 0.1) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hud-coordinate {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 20px;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.65rem;
|
||||
color: var(--bg-color);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.hud-coordinate::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
top: 50%;
|
||||
width: 20px;
|
||||
height: 1px;
|
||||
background: var(--terminal-line-color);
|
||||
}
|
||||
|
||||
.data-stream {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
right: 20px;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.6rem;
|
||||
color: var(--status-success);
|
||||
opacity: 0.5;
|
||||
z-index: 9999;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.md-nav__link.nav-primary,
|
||||
.md-nav__link.nav-secondary {
|
||||
font-family: var(--code-font);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admonition-terminal {
|
||||
border-left: 4px solid var(--accent-green);
|
||||
background: linear-gradient(180deg,
|
||||
transparent 0%,
|
||||
rgba(0, 255, 0, 0.05) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.admonition-terminal .admonition-title {
|
||||
font-family: var(--code-font);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
background: var(--terminal-green-glow);
|
||||
}
|
||||
|
||||
.file-tree {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.8rem;
|
||||
padding-left: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.system-metadata {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.65rem;
|
||||
text-align: right;
|
||||
color: var(--text-muted);
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
pre.highlight {
|
||||
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
pre.highlight .hll {
|
||||
background-color: rgba(0, 255, 0, 0.05);
|
||||
}
|
||||
|
||||
@keyframes matrixFall {
|
||||
0% { transform: translateY(-100vh); opacity: 0; }
|
||||
10% { opacity: 0.5; }
|
||||
90% { opacity: 0.5; }
|
||||
100% { transform: translateY(100vh); opacity: 0; }
|
||||
}
|
||||
|
||||
.matrix-char {
|
||||
position: fixed;
|
||||
color: var(--accent-green);
|
||||
font-family: var(--code-font);
|
||||
font-size: 1rem;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
animation: matrixFall 15s linear infinite;
|
||||
}
|
||||
|
||||
.palette-section {
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
.palette-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.palette-item {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 1.5em;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.palette-item:hover {
|
||||
border-color: var(--accent-green);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 0, 0.2);
|
||||
}
|
||||
|
||||
.palette-item .icon-label {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.palette-item a {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.network-viz {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.network-viz::before,
|
||||
.network-viz::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--accent-green);
|
||||
}
|
||||
|
||||
.network-viz::before {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: networkSpin 20s linear infinite;
|
||||
}
|
||||
|
||||
.network-viz::after {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
animation: networkSpin 15s linear infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes networkSpin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.terminal-decorator {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted);
|
||||
z-index: 9999;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.system-notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--bg-color);
|
||||
border-left: 4px solid var(--accent-green);
|
||||
padding: 1em 1.5em;
|
||||
border-radius: 4px;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.8rem;
|
||||
box-shadow: 0 8px 24px rgba(0, 255, 0, 0.15);
|
||||
z-index: 10000;
|
||||
min-width: 300px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body::before,
|
||||
body::after,
|
||||
.command-palette::before,
|
||||
.command-palette::after,
|
||||
.matrix-char,
|
||||
.network-viz::before,
|
||||
.network-viz::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-main__inner {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen {
|
||||
[data-md-color-scheme="slate"] {
|
||||
--md-default-bg-color: var(--bg-primary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
:root {
|
||||
--crt-amber: #e7d9a3;
|
||||
--crt-blue: #00d9ff;
|
||||
--crt-green: #00f080;
|
||||
--crt-red: #ff4c4c;
|
||||
--phosphor-dim: #1a3a1a;
|
||||
--status-success: #4ade80;
|
||||
--status-warning: #fbbf24;
|
||||
--status-error: #ef4444;
|
||||
--status-info: #60a5fa;
|
||||
--terminal-green-glow: rgba(0, 255, 0, 0.15);
|
||||
--terminal-line-color: rgba(0, 255, 0, 0.3);
|
||||
--terminal-border-faint: rgba(0, 255, 0, 0.08);
|
||||
--md-default-bg-color: rgba(0, 255, 0, 0.08);
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
|
||||
background: linear-gradient(
|
||||
rgba(18, 32, 12, 0) 50%,
|
||||
rgba(0, 20, 0, 0.1) 50%
|
||||
),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(12, 36, 18, 0) 50%,
|
||||
rgba(0, 16, 0, 0.08) 50%
|
||||
);
|
||||
|
||||
background-size: 100% 4px, 6px 100%;
|
||||
animation: scanline 4ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
0% { background-position: 0 0, 0 0; }
|
||||
100% { background-position: 0 4px, 6px 100%; }
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
transparent 50%,
|
||||
rgba(0, 0, 0, 0.3) 100%
|
||||
);
|
||||
}
|
||||
|
||||
@keyframes cursorBlink {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.md-header__topic::before,
|
||||
body > *:first-child::before {
|
||||
content: "λ ";
|
||||
animation: cursorBlink 1s step-end infinite;
|
||||
color: var(--crt-amber);
|
||||
font-family: var(--code-font);
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
background: linear-gradient(180deg,
|
||||
var(--bg-primary) 0%,
|
||||
var(--bg-card) 100%
|
||||
);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1.5em;
|
||||
position: relative;
|
||||
box-shadow: 0 8px 32px rgba(0, 255, 0, 0.1);
|
||||
}
|
||||
|
||||
.terminal-window::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
var(--accent-green),
|
||||
transparent
|
||||
);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
strong {
|
||||
text-shadow: 0 0 1px var(--border-color);
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: var(--crt-amber);
|
||||
font-family: var(--code-font);
|
||||
font-weight: bold;
|
||||
margin-right: 0.02em;}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
color: var(--crt-amber);
|
||||
font-family: var(--code-font);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p strong,
|
||||
strong {
|
||||
text-shadow: 0 0 1px var(--border-color);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: linear-gradient(180deg,
|
||||
var(--bg-color) 0%,
|
||||
#0d150d 100%
|
||||
);
|
||||
border-left: 3px solid var(--accent-green);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
pre[role="presentation"] [data-line] {
|
||||
color: var(--text-muted);
|
||||
font-family: var(--code-font);
|
||||
user-select: none;
|
||||
min-width: 3em;
|
||||
text-align: right;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
pre code .token-keyword {
|
||||
color: var(--crt-blue);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre code .token-string {
|
||||
color: var(--status-success);
|
||||
}
|
||||
|
||||
pre code .token-comment {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kbd,
|
||||
kbd,
|
||||
[data-type="kbd"] {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.2em 0.5em;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.85em;
|
||||
color: var(--accent-green);
|
||||
text-shadow: 0 1px 2px rgba(0, 255, 0, 0.1);
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
kbd:hover {
|
||||
border-color: var(--accent-green);
|
||||
box-shadow: 0 2px 8px rgba(0, 255, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.85em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--status-success);
|
||||
background: rgba(74, 222, 128, 0.1);
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
color: var(--status-warning);
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--status-error);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.status-info {
|
||||
color: var(--status-info);
|
||||
background: rgba(96, 165, 250, 0.1);
|
||||
border: 1px solid rgba(96, 165, 250, 0.3);
|
||||
}
|
||||
|
||||
.admonition {
|
||||
border-left: 4px solid var(--accent-green);
|
||||
background: linear-gradient(180deg,
|
||||
transparent 0%,
|
||||
rgba(0, 255, 0, 0.03) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.admonition-title {
|
||||
font-family: var(--code-font);
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
color: var(--accent-green);
|
||||
background: var(--terminal-green-glow);
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.md-typeset details.admonition {
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.md-typeset table:not([class]) {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.md-typeset__table th {
|
||||
background: linear-gradient(180deg,
|
||||
var(--bg-secondary) 0%,
|
||||
var(--bg-card) 100%
|
||||
);
|
||||
border-bottom: 2px solid var(--accent-green);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.md-typeset__table tr:hover {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
blockquote, .admonition-blockquote {
|
||||
border-left: 3px solid var(--accent-green);
|
||||
margin: 1.5em 0;
|
||||
padding-left: 1.5em;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(0, 255, 0, 0.05) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
blockquote p:first-of-type::before {
|
||||
content: "› ";
|
||||
color: var(--accent-green);
|
||||
font-family: var(--code-font);
|
||||
}
|
||||
|
||||
.md-typeset a::after,
|
||||
a:not(.btn)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: var(--bg-card);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.md-typeset a:hover::after,
|
||||
a:not(.btn):hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-family: var(--code-font);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 0.85em;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.md-typeset .admonition-title,
|
||||
div.admonition > div:first-of-type::before {
|
||||
font-family: var(--code-font);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.md-typeset mark,
|
||||
.highlight-mark {
|
||||
background-color: rgba(255, 235, 0, 0.3);
|
||||
color: inherit;
|
||||
padding: 0.1em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--terminal-green-glow);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-green-dim);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body::before {
|
||||
background-size: 100% 2px, 3px 100%;
|
||||
animation-duration: 6s;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--accent-green: #fff;
|
||||
--bg-primary: #000;
|
||||
}
|
||||
|
||||
body::before {
|
||||
background-size: 100% 3px, 4px 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes terminalLoad {
|
||||
0% { opacity: 0; transform: translateY(25px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.md-typeset h1,
|
||||
.md-content {
|
||||
animation: terminalLoad 200ms ease-out forwards;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 9997;
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
rgba(0, 255, 0, 0.03) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
animation: phosphorDecay 8s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes phosphorDecay {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
body::after::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
transparent 40%,
|
||||
rgba(0, 0, 0, 0.6) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.terminal-banner {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.75em;
|
||||
letter-spacing: 0.2em;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 1em 0;
|
||||
border-top: 1px dashed var(--terminal-line-color);
|
||||
border-bottom: 1px dashed var(--terminal-line-color);
|
||||
margin: 3em 0;
|
||||
}
|
||||
|
||||
.holographic-grid {
|
||||
background-image:
|
||||
linear-gradient(var(--terminal-border-faint) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--terminal-border-faint) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.holographic-grid::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(
|
||||
rgba(0, 255, 0, 0.03) 10px,
|
||||
transparent 10px
|
||||
),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 255, 0, 0.03) 10px,
|
||||
transparent 10px
|
||||
);
|
||||
background-size: 10px 10px;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
.md-sidebar {
|
||||
background-color: var(--bg-color) !important;
|
||||
}
|
||||
|
||||
.md-sidebar__scrollwrap {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.md-header__topic {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: -0.02em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.md-typeset h1::before,
|
||||
div[data-md-component="content"] > h1::before {
|
||||
content: 'λ ';
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono';
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.md-typeset h2::before,
|
||||
div[data-md-component="content"] > h2::before {
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono';
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.md-typeset h3::before,
|
||||
div[data-md-component="content"] > h3::before {
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono';
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.md-header__button.md-icon {
|
||||
padding: 0.5em;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
nav[data-md-level="1"] .md-nav__link,
|
||||
nav[data-md-level="2"] .md-nav__link {
|
||||
font-family: var(--code-font);
|
||||
font-size: 0.9rem;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.md-sidebar.nav-primary > nav > .md-nav__list > li > .md-nav__link {
|
||||
font-weight: 600;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.md-sidebar.nav-primary > nav > .md-nav__list > li.is-parent > .md-nav__nested > li > a {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.md-sidebar.nav-primary > nav > .md-nav__list > li.is-parent > .md-nav__nested > li > a:hover,
|
||||
.md-sidebar.nav-primary > nav > .md-nav__list > li.is-parent > .md-nav__nested > li > a.md-nav--opened {
|
||||
color: var(--crt-red);
|
||||
}
|
||||
|
||||
.md-sidebar__section.md-nav__inner > .md-nav__title {
|
||||
font-family: var(--text-primary);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-muted);
|
||||
padding: 0.75em 1.5em 0.25em;
|
||||
}
|
||||
|
||||
.md-sidebar__inner > .md-nav__title {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--crt-red);
|
||||
padding: 1em 1.5em;
|
||||
}
|
||||
|
||||
.md-sidebar__inner > .md-nav__title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.md-typeset {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.quick-access,
|
||||
div[data-md-include]:has(h3:contains("Quick Access")) {
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: 3em;
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.md-typeset .md-typeset__grid,
|
||||
div[data-md-type="grid"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
.quick-access-card,
|
||||
.md-typeset__section:not(:last-child) {
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 1.25em;
|
||||
}
|
||||
|
||||
.quick-access-card h3 {
|
||||
font-family: var(--code-font);
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--crt-green);
|
||||
}
|
||||
|
||||
.quick-access-card > p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.quick-access-card .md-icon,
|
||||
.quick-access-card svg {
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
color: var(--crt-green);
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.quick-access-card a:hover {
|
||||
text-decoration-color: var(--crt-green);
|
||||
}
|
||||
|
||||
.md-footer-meta {
|
||||
background-color: var(--bg-primary) !important;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
pre[role="presentation"] [data-line] {
|
||||
padding-left: calc(var(--per-page) * 0.75em + 1.25em);
|
||||
}
|
||||
|
||||
.md-typeset a {
|
||||
color:var(--crt-green);
|
||||
word-break:break-word
|
||||
}
|
||||
|
||||
.md-typeset h1,
|
||||
.md-typeset h2,
|
||||
.md-typeset h3 {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
title: Notice
|
||||
---
|
||||
# Anonymous Planet has left Twitter
|
||||
|
||||
Anonymous Planet has moved to Mastodon. This was largely due to Twitter being owned by Elon Musk, the $8 fees, and recent security issues that have come to light. We do not regret this decision, as it has only gotten worse for Twitter users since.
|
||||
|
||||
- [Twitter $8 Fee Exploited by Cybercriminals](https://heimdalsecurity.com/blog/twitter-8-fee-exploited-by-cybercriminals/)
|
||||
|
||||
Our mastodon can be found at [@anonymousplanet@mastodon.social](https://mastodon.social/@anonymousplanet)
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "Verifying authenticity"
|
||||
---
|
||||
|
||||
<div style="font-family: var(--code-font); color: var(--crt-red); font-size: 0.9rem; margin-bottom: 2em;">
|
||||
Never blindly trust the information you see online.
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5em; max-width: 1000px; margin: 0 auto;">
|
||||
|
||||
<div class="quick-access-card">
|
||||
<h4 style="font-family: var(--code-font); font-size: 1.1rem; margin: 0;">Get our keyring</h4>
|
||||
<p style="margin: 0 0 1em;">The Anonymous Planet MSK and other keys in our keyring can be found here.</p>
|
||||
<a href="../../pgp/" style="font-family: var(--code-font); font-size: 0.85rem; color: var(--crt-amber);">Access our public keyring <span style="font-size: 0.7em;"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Files Provided
|
||||
|
||||
For each release, you'll receive:
|
||||
|
||||
| File Type | Purpose | Verification Command |
|
||||
|-----------|---------|---------------------|
|
||||
| **PDF** (`thgtoa.pdf`) | The actual guide document | Check hash + signature |
|
||||
| **.sig file** | GPG detached signature for authenticity | `gpg --verify file.sig file.pdf` |
|
||||
| **.sha256** | SHA256 checksum for integrity | `sha256sum -c file.sha256` |
|
||||
|
||||
## Quick Verification
|
||||
|
||||
### Using Python Script (Recommended)
|
||||
|
||||
```sh
|
||||
# Verify everything (hashes, signatures, and optionally VirusTotal)
|
||||
python scripts/verify_pdf.py --all
|
||||
|
||||
# Only verify hashes
|
||||
python scripts/verify_pdf.py --hashes
|
||||
|
||||
# Only verify GPG signatures
|
||||
python scripts/verify_pdf.py --signatures
|
||||
|
||||
# Check VirusTotal scan status (requires VT_API_KEY environment variable)
|
||||
python scripts/verify_pdf.py --vt
|
||||
```
|
||||
|
||||
### Manual Verification
|
||||
|
||||
#### 1. Verify SHA256 Hash
|
||||
|
||||
**Linux/macOS:**
|
||||
|
||||
```sh
|
||||
cd /path/to/repo
|
||||
sha256sum -c sha256sum-light.txt
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
Get-FileHash -Algorithm SHA256 export\thgtoa.pdf | Select-Object Hash
|
||||
# Compare with the hash in thgtoa.pdf.sha256
|
||||
```
|
||||
|
||||
#### 2. Verify GPG Signature
|
||||
|
||||
First, import the public key:
|
||||
|
||||
```sh
|
||||
gpg --import pgp/anonymousplanet.asc
|
||||
```
|
||||
|
||||
Then verify the signature:
|
||||
|
||||
```sh
|
||||
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
|
||||
gpg --verify export/thgtoa-dark.pdf.sig export/thgtoa-dark.pdf
|
||||
```
|
||||
|
||||
**Example output for successful verification:**
|
||||
|
||||
```text
|
||||
gpg: Signature made Mon 20 Apr 2026 01:46:40 AM EDT
|
||||
gpg: using EDDSA key 9FA5436D0EE360985157382517ECA05F768DEFDA
|
||||
gpg: Good signature from "Anonymous Planet Master Signing Key" [unknown]
|
||||
gpg: WARNING: This key is not certified with a trusted signature!
|
||||
gpg: There is no indication that the signature belongs to the owner.
|
||||
Primary key fingerprint: 9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6
|
||||
```
|
||||
|
||||
**Note:** The "WARNING" is expected - it means the key hasn't been signed by another trusted key. This is normal for independent signing keys.
|
||||
|
||||
#### 3. Check VirusTotal Status
|
||||
|
||||
Visit the VirusTotal report links (automatically generated in release notes):
|
||||
- Light mode: `https://www.virustotal.com/gui/file/[hash]`
|
||||
- Dark mode: `https://www.virustotal.com/gui/file/[hash]`
|
||||
|
||||
Or use the Python script with API key:
|
||||
|
||||
```sh
|
||||
export VT_API_KEY=your_vt_api_key
|
||||
python scripts/verify_pdf.py --vt
|
||||
```
|
||||
|
||||
## Automated Verification in CI/CD
|
||||
|
||||
The GitHub Actions workflows automatically:
|
||||
|
||||
1. **Build PDFs** from MkDocs source
|
||||
2. **Generate SHA256 hashes** and save to root directory
|
||||
3. **Sign files with GPG** using the repository's private key
|
||||
4. **Scan with VirusTotal** and update release notes
|
||||
5. **Create releases** with all verification artifacts
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Always verify signatures** before opening PDFs from untrusted sources
|
||||
2. **Check hashes** to ensure files weren't corrupted during download
|
||||
3. **Review VirusTotal results** for any suspicious detections
|
||||
4. **Import keys securely** - verify key fingerprints with the project maintainers
|
||||
5. **Keep verification scripts updated** to match current security standards
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Good signature" but wrong owner?
|
||||
|
||||
- Ensure you imported the correct public key from [`pgp/`](../pgp/index.md)
|
||||
- Check the key fingerprint matches the official one from the repository announcements
|
||||
|
||||
### Hash mismatch?
|
||||
|
||||
- Re-download the file (corruption during transfer)
|
||||
- Verify you're checking against the correct hash file for the mode (light/dark)
|
||||
- Check for disk errors on your system
|
||||
|
||||
### GPG not found?
|
||||
|
||||
- **Linux/Debian:** `sudo apt install gnupg`
|
||||
- **Linux/RHEL/CentOS:** `sudo yum install gnupg2` or `sudo dnf install gnupg2`
|
||||
- **macOS:** `brew install gnupg` or use Homebrew Casks: `brew install --cask gnupg`
|
||||
- **Windows:** Use [Gpg4win](https://www.gpg4win.org/)
|
||||
@@ -0,0 +1,2 @@
|
||||
39e7f8098d6c9511b98f83f4548ef8bac0d604fe820c4dbe1f731dbdff47676c0800872ba329492427cdfdf66734f55d03e3b4dd95b48e9e2ca2b3b4cd716213 thgtoa.pdf
|
||||
ba29fcd4ee9bd43a7ed96752bc372f7d374d69f3d37e33e04d07fd14fe4e62afccbc05471e8ad89632d31045a56eee9bde7c15a0c405f64c977e5e4ac30654fa thgtoa-dark.pdf
|
||||
@@ -0,0 +1,2 @@
|
||||
ad7b3e327559dd835755615103bb1c59ef6f41ba652f6ee40c8fcdd082914f49 thgtoa.pdf
|
||||
1174ec6f1e074b6b0115cea54ee135e82e56771d7129dcf367037a7020d5b39c thgtoa-dark.pdf
|
||||
+69308
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEABYKAB0WIQTDAj2+o/s4xDi6Hu3Oxgrt6LmSogUCahwNewAKCRDOxgrt6LmS
|
||||
opw7AQDdsg3JaS2vy2ZYCI4L1F+guKHF/zItJUSTj76DdOVzSAD+PKDCa4Io6OO9
|
||||
7v2odiJHOrbYNmte5FhhffUZL8Nz1A4=
|
||||
=oBfF
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1 @@
|
||||
ba29fcd4ee9bd43a7ed96752bc372f7d374d69f3d37e33e04d07fd14fe4e62afccbc05471e8ad89632d31045a56eee9bde7c15a0c405f64c977e5e4ac30654fa
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQTDAj2+o/s4xDi6Hu3Oxgrt6LmSogUCahwDIRsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQzsYK7ei5kqJkrQD/etBsZk8BI71Dn0mgTDIQ
|
||||
HaYuAqtld5MmKaV9AxlniWABANt6V/0ivcXSsxajFdvpdu4TI9D4GR07ZeKFjYXV
|
||||
EZsM
|
||||
=/p57
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQTDAj2+o/s4xDi6Hu3Oxgrt6LmSogUCahwDHxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQzsYK7ei5kqIsEgD+PNgOOJy7GPQUYuaDlxeh
|
||||
ldQWf58ivLfQ6zpgeSSTiqIA/19EDw+Un9AYuxikZGp39vcNFxEhnwD7dRWZo/Ie
|
||||
ZyAE
|
||||
=OrTx
|
||||
-----END PGP SIGNATURE-----
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEABYKAB0WIQTDAj2+o/s4xDi6Hu3Oxgrt6LmSogUCahwNegAKCRDOxgrt6LmS
|
||||
ov0tAQCNiaIONY2A6zRVXUcOolOOCJY1pi9SvuJ/yalbTQewawEAsi7bhFYAo6c0
|
||||
yAy/jBcGD5E5HzLlmjkGvYcwsvWPfQo=
|
||||
=lnwq
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1 @@
|
||||
39e7f8098d6c9511b98f83f4548ef8bac0d604fe820c4dbe1f731dbdff47676c0800872ba329492427cdfdf66734f55d03e3b4dd95b48e9e2ca2b3b4cd716213
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQTDAj2+o/s4xDi6Hu3Oxgrt6LmSogUCahwDIBsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQzsYK7ei5kqJ6/QEAk2Ta0gygpWKSKstLjKwX
|
||||
wmqIyrEza93Xk22owhYi3FAA/jQslZb0MahgPZyf3PQ8syUlBJS8gKQ8nBEpf5BO
|
||||
Q/EK
|
||||
=Fvmv
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQTDAj2+o/s4xDi6Hu3Oxgrt6LmSogUCahwDHxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQzsYK7ei5kqIN4gEA2T011PhyNNqhGcj0uVTD
|
||||
47AZKLxWhZXnLzD0sRUHY/oBAMWFfSXrKN5q8yml5dWLbvFqbcIpefgHD8smBd6v
|
||||
fzUH
|
||||
=3Cxi
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,16 @@
|
||||
## VirusTotal Scan Results
|
||||
|
||||
**Scan Date:** 2026-04-19 01:48 UTC
|
||||
|
||||
---
|
||||
|
||||
### thgtoa.pdf
|
||||
- **SHA256 Hash:** `f82f6f53319315568fc2524b4eaf01126fe52356a20363cd358ad5977388ba28`
|
||||
- **VirusTotal Report:** VT_API_KEY not configured, scan skipped
|
||||
|
||||
### thgtoa-dark.pdf
|
||||
- **SHA256 Hash:** `94a0c8e3b81b0aeeb921029a41713d81b836da893a9bc9f905ca7296e82bd70f`
|
||||
- **VirusTotal Report:** VT_API_KEY not configured, scan skipped
|
||||
|
||||
---
|
||||
*Scan performed automatically by GitHub Actions*
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
|
||||
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fYpCgEA209U3QewChp7mdrrFjH1
|
||||
CaBMIk2sCHwRMCcmbMDkNTAA/RIchAKex13ZjZWC9xsJpZEktvBENFsQLsNPReqR
|
||||
UZ8C
|
||||
=TYsa
|
||||
-----END PGP SIGNATURE-----
|
||||
+38
-27
@@ -1,14 +1,15 @@
|
||||
site_name: Hitchhiker's Guide
|
||||
site_name: The Hitchhiker's Guide
|
||||
site_author: Anonymous Planet
|
||||
site_description: "The comprehensive guide for online anonymity and OpSec."
|
||||
site_dir: '/site/'
|
||||
docs_dir: 'docs/'
|
||||
site_url: "https://www.anonymousplanet.org/"
|
||||
site_description: "The comprehensive guide for online #anonymity and #opsec."
|
||||
site_dir: "site"
|
||||
docs_dir: "docs"
|
||||
site_url: "https://anonymousplanet.net/"
|
||||
repo_url: "https://github.com/Anon-Planet/thgtoa"
|
||||
repo_name: ""
|
||||
#edit_uri: ""
|
||||
theme:
|
||||
name: material
|
||||
locale: en
|
||||
favicon: media/profile.png
|
||||
icon:
|
||||
logo: material/bird
|
||||
@@ -17,8 +18,11 @@ theme:
|
||||
text: Public Sans
|
||||
code: Liberation Mono
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.instant.prefetch
|
||||
- navigation.tabs
|
||||
- navigation.sections
|
||||
- navigation.path
|
||||
- toc.integrate
|
||||
- navigation.top
|
||||
- search.suggest
|
||||
@@ -28,32 +32,30 @@ theme:
|
||||
- content.code.copy
|
||||
language: en
|
||||
palette:
|
||||
- scheme: default
|
||||
toggle:
|
||||
icon: material/eye-outline
|
||||
name: Switch to dark mode
|
||||
primary: green
|
||||
accent: blue
|
||||
- scheme: slate
|
||||
toggle:
|
||||
icon: material/eye
|
||||
name: Switch to light mode
|
||||
primary: grey
|
||||
accent: teal
|
||||
primary: hacker
|
||||
accent: green
|
||||
|
||||
plugins:
|
||||
- social: {}
|
||||
# - with-pdf:
|
||||
- search:
|
||||
separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
||||
# - macros: {}
|
||||
# - meta: {}
|
||||
# - git-latest-tag: {}
|
||||
# - git-authors: {}
|
||||
# - git-latest-release: {}
|
||||
# - meta: {}
|
||||
|
||||
# Soon this will all be minified (It's a personal WIP)
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
- stylesheets/hacker.css
|
||||
- stylesheets/hacker-extra.css
|
||||
- stylesheets/going-dark.css
|
||||
- stylesheets/navigation.css
|
||||
- stylesheets/footer.css
|
||||
- stylesheets/accessibility.css
|
||||
|
||||
# extra_css:
|
||||
# - stylesheets/hacker-core.css
|
||||
# - stylesheets/components.css
|
||||
# - stylesheets/accessibility.css
|
||||
# - stylesheets/print-and-utilities.css
|
||||
|
||||
extra:
|
||||
social:
|
||||
@@ -64,7 +66,7 @@ extra:
|
||||
link: http://wmj5kiic7b6kjplpbvwadnht2nh2qnkbnqtcv3dyvpqtz7ssbssftxid.onion/
|
||||
name: "0xacab"
|
||||
- icon: simple/gitea
|
||||
link: http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonymousplanetorg
|
||||
link: http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonypla
|
||||
name: Darktea
|
||||
- icon: simple/github
|
||||
link: https://github.com/anon-planet
|
||||
@@ -118,8 +120,17 @@ markdown_extensions:
|
||||
- tables: {}
|
||||
- footnotes: {}
|
||||
- toc:
|
||||
permalink: true
|
||||
permalink: "λ"
|
||||
toc_depth: 3
|
||||
|
||||
copyright: |
|
||||
© 2023-2025 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a>
|
||||
nav:
|
||||
- "Home": index.md
|
||||
- "Verify": verify/index.md
|
||||
- "Guide": guide/index.md
|
||||
- "Code": code/index.md
|
||||
- "Donations": contribute/index.md
|
||||
- "A Constitution": constitution/index.md
|
||||
- "Mirrors": mirrors/index.md
|
||||
- "Changelog": changelog/index.md
|
||||
- "About": about/index.md
|
||||
- "PGP": pgp/index.md
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGNnDKYBEADEwpJcPVDJLJHlaRtBtVVJ2p2SaNwbZKgeI2zfpiLu4rTmpxUp
|
||||
cbyW5S3mI++kGt4ljcKTzQM0+upr2hcdZi/rpwliHLOxsC32cvTy4YtPmoKdOalo
|
||||
blJ9+llDbl0lBBvnqQcqFhnDMPXQPsaewWmCpGjwCwnQpxXLWmKhTYMxoQtzzZ8U
|
||||
oagorLwASkb6+NZoha96ayDlE41KNErI51U8qiVxMR+8iN8pcJ1l3XA9bfMKBz45
|
||||
TnlaoJ391CvJUgJ9535FjifmOyWTB0OYgJptMPz+n0K5jTOE7mvoqT6a/hqbAGDp
|
||||
5i5LgSYVPfJqZsdrkQBMwO5pW9XymH7hNHPhaX6nPkDB8RLKexqso9pzLapG8WNC
|
||||
sk+jxTC77TOFh9CniGks7UZoa0pRdhA5sGD0Wjh8eWgDRqdgYEmqviuulWnJDti0
|
||||
dIQNixzh+TylEO8YNJyz49KUIr/ckapHfPI1BZWUyZZLpcvNvT/2IzcEeT3Tgmfr
|
||||
IZsk2U91kA9z+BKEx8mJ7V5KZo7ku0uVgAtQn5oyluSIptUGwYu5DqhnZAqKXZok
|
||||
S7i2NMghrPMM/Wf048VXuxO1Dx7CwP7Q1LCNhwL0jsLWtXIJVm7NtTt+1Vj/M4EH
|
||||
Fl4g0B7iK6JiZEPYEp5YGSWpyhpSTKQaOOCHHKSCIjVx6VLm+/Xbaf6/TwARAQAB
|
||||
tEtBbm9ueW1vdXMgUGxhbmV0IEVtYWlsIEVuY3J5cHRpb24vU2lnbmluZyBLZXkg
|
||||
PGNvbnRhY3RAYW5vbnltb3VzcGxhbmV0Lm9yZz6JAk4EEwEKADgWIQS20XV2MqKA
|
||||
+Z8ty/25q52Tr/BbnAUCY2cMpgIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAK
|
||||
CRC5q52Tr/BbnOA7EACeevkcNYbacvNJx+E8cAHyVRS7kuSWDfV0EvCeiCsZ3+sq
|
||||
q9CFADbBn4wXuELdFcPSME5UGOMpZ3MVwWocHyRrU+axseO/OCgbl15nxgk3lsSK
|
||||
Tew/1YHnjTIfpDkSOw5kT86yxea9/bpIWVzb1aCkKxVogr1cXzvBdYRWV5qC3BP4
|
||||
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
|
||||
J0nsuzwoKTRfhIDlYN7e
|
||||
=Mkw3
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,14 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEY2cLxRYJKwYBBAHaRw8BAQdA1wWVN04/7B2thXG3Ppm9nj9BXOosgFUCq+6m
|
||||
7q7jDUG0QUFub255bW91cyBQbGFuZXQgTWFzdGVyIFNpZ25pbmcgS2V5IChodHRw
|
||||
czovL2Fub255bW91c3BsYW5ldC5vcmcpiJAEExYKADgWIQSeqYJ4Y58c2FPglsv/
|
||||
lFB1h6apuQUCY2cLxQIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRD/lFB1
|
||||
h6apuVvhAP0UTSY/QchH8LfHaw1inGaViik9rALbjdBeVRWofwyRSQD8DH2LRX3v
|
||||
f/DgBOK7Li6OL05s9wsEYwoF+8B1qWJinQu4OARjZwvFEgorBgEEAZdVAQUBAQdA
|
||||
xO3KbSonM28D2uTNHpXFRneFL3LqUO+8JW14eULOdxoDAQgHiHgEGBYKACAWIQSe
|
||||
qYJ4Y58c2FPglsv/lFB1h6apuQUCY2cLxQIbDAAKCRD/lFB1h6apuZ32AQCiiR0d
|
||||
bD29xEmQYf4b9F77jAdFFr2DoEGjeZBPoTrJywEA8m1dD5ZOS0qn1Yz3WkTgBflL
|
||||
/0VkU6m06r/KxLL4fg0=
|
||||
=4NMF
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,16 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEY2cNGBYJKwYBBAHaRw8BAQdAbKn/ExAQ+aq6/o2yc04B9jx5PMloaxux1eoT
|
||||
iKwQgX60JEFub255bW91cyBQbGFuZXQgUmVsZWFzZSBTaWduaW5nIEtleYiQBBMW
|
||||
CgA4FiEEg6bPnvV6wltcf10pKF5gSKEjIbIFAmNnDRgCGwMFCwkIBwMFFQoJCAsF
|
||||
FgIDAQACHgECF4AACgkQKF5gSKEjIbI5+QD/YSQ5E+LW4YJEAQQ+D3LFsGtGGRf3
|
||||
qQRD5plsUvTtBfsA/15EJaIjzSwrsf/3wsW48zSYKCer/nrhGY9y5yd0m2gBiHUE
|
||||
EBYKAB0WIQSeqYJ4Y58c2FPglsv/lFB1h6apuQUCY2cNxAAKCRD/lFB1h6apuXun
|
||||
AQCSNwZBNybUZzN/K4Zl1j6uhCqqnvbUlO80wvbHDMXpywD/dpabqjmpfxfJC20n
|
||||
t3OFxKSeIbfJ0VHvoHKpwcaGuwC4OARjZw0YEgorBgEEAZdVAQUBAQdAE7WMDHTx
|
||||
zWp542lXGLxSsiE4gtMvVxkEneKmZWwzbDcDAQgHiHgEGBYKACAWIQSDps+e9XrC
|
||||
W1x/XSkoXmBIoSMhsgUCY2cNGAIbDAAKCRAoXmBIoSMhsowLAP42HbiJIsIodWwn
|
||||
C3yBzwGrd1xRtf/91MpQUgFpCx7xuAD9G0F3l04hKkjxiHK+wJ27LnYcigaTVdje
|
||||
6d7bt7TerwE=
|
||||
=Hgos
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,89 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEZc0QYxYJKwYBBAHaRw8BAQdAm8mOR8/0qWrm9Tqzfl9Ks5rjtIbQZLAR/qxH
|
||||
HVGJsxi0LUFub255bW91cyBQbGFuZXQgRW1haWwgRW5jcnlwdGlvbi9TaWduaW5n
|
||||
IEtleYiTBBMWCgA7FiEE/L0sq979H7ounnWRoags0t0s+JAFAmXNEGMCGwMFCwkI
|
||||
BwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQoags0t0s+JDbRAEAuZlBmMGgZ3bh
|
||||
12Js9jjDcu+jhKqL4fJrJG5z9+KFkQwA/An1StA6EhcM7qlzZ5bzm2SZAbP9hQRZ
|
||||
GmfaeU2P5KgHiHgEMBYKACAWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaiCYTgId
|
||||
AAAKCRAX7KBfdo3t9gNUAP9/SyGBYJ7s9YeqLHOJ+veQZjZYHvFGQ7yPn0Fetx0Z
|
||||
LAD/UOQ8rP2QaldCMyVSG8SqfPd7n++SEAXWAl2gAo9mhg6IdQQQFgoAHRYhBJ+l
|
||||
Q20O42CYUVc4JRfsoF92je32BQJmpEUQAAoJEBfsoF92je32tD8A/ir9hE8UjrJE
|
||||
psG+PNfxYAwAagKUGbAMDUxQp3z+t81+AP45hYT4aR89zSQaankHLs3Lh7Cp5ael
|
||||
NBe/BtfR9hCLAYkCMwQQAQgAHRYhBF7WeRgs0hkTAMDm6kyyELegkVLWBQJqIJiM
|
||||
AAoJEEyyELegkVLWjZYP/j1k5vl+r0NDQXmE8hS9IKhaQPggP72iXc5RWeMQHuIv
|
||||
b1laQZm64xerJNdAh0uk1bwfmJnVGfyxBUrlCgAIeVGRSlni2Rig4azaQ1IS0pqF
|
||||
4sC1KzKEhEaNdkh3pJyGtP1cikcSjWeU2oYQou3/7VN3vNyW+n8OAVF+2fsC5d78
|
||||
EvdpZgal+komb+J8Bt552uDbCCVI4TFIPBZmHWoXjaP6L+730YphbV7Aw0L5J6OO
|
||||
ob0nzHn4X0dIvGE7Phdp2e1yNRUOSRLh8B/D5OiE9k7CaeYmJNPv5qOw/R+NgrrA
|
||||
ZFnoOuwHo0D+aL9WT9q4aM/cDCEIbvhQ4l5ZhVGqZuQ9wxNCgPi3ZiZRTfk1PW4v
|
||||
uMw1xGwXBKy7jDO12xWIWWv9MiwIQLw0OxSxKbr76rgucq7e7JrWr64rItu5Wm7F
|
||||
8qxg2cwmDat6tFSRVWlEDy8oNkRMJNjdQJDu3ez9YOfJNnApAz94Of1XU7CUuYjY
|
||||
PV88BaHdUBVtANEzy0iSDCcSj6auzLfv9dBN8cOdUxlVcrPf2jjK6JR/6qe6VWNp
|
||||
wRg9VQW2fe8HJTMUt0o9qQBJUsF68KOHtIdoE4az9AyyBNKl67dKqLB9HoIItLzD
|
||||
MJRcbS2p6plCTNagwPVvgtPRChll9JP3jLPVhRL2BixYVkbHUoJxsEfscTUl6Azt
|
||||
tEtBbm9ueW1vdXMgUGxhbmV0IEVtYWlsIEVuY3J5cHRpb24vU2lnbmluZyBLZXkg
|
||||
PGFub255bW91c3BsYW5ldEBkaXNyb290Lm9yZz6IkwQTFgoAOxYhBPy9LKve/R+6
|
||||
Lp51kaGoLNLdLPiQBQJmhqXqAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheA
|
||||
AAoJEKGoLNLdLPiQJ34A/RJT9Hyj7hT/0D1BbDU6s6YzD+/x7Pyq2+9kNSI0L77W
|
||||
AQAAG+CfDrKDXJtBNKZVNFZpld3wUeoIOcAqLl7KpsVGCYh1BBAWCgAdFiEEn6VD
|
||||
bQ7jYJhRVzglF+ygX3aN7fYFAmakRRAACgkQF+ygX3aN7fbypgEAnEg0IbWnpaLj
|
||||
/4wU179vUZZu/Y0DE63GbJuZjj72hKUA/0xyzIgSvXByjoOkEwCn5w1+RPYXKw7Z
|
||||
syERsDCUAAMIiQIzBBABCAAdFiEEXtZ5GCzSGRMAwObqTLIQt6CRUtYFAmogmIwA
|
||||
CgkQTLIQt6CRUtafPQ/+LTWFU84tDZAM0Hp7bWB0dw8nP0JvNQ2WtZf0flh+r1tF
|
||||
cmVnc9szZBh+zzSpY25iK5+Waa6+l1POYSQpkS67VR0Jrv9nL94YrRhqalSRWsjW
|
||||
MQJO+Obu4LIRIqiMZLJlAd9Bg9FshYagbQDVDOI8v9mxqCzIVm3tBx1Jp57ATHgm
|
||||
sMDWn7l1BI0SkLlG49LYxVDQ6QAx4XLCQw+JzdiJs+yExa5ymYmV61evVVbDV5UF
|
||||
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
|
||||
Eztpvc4cyyGAmI42SLM/jKbqO2yWqwVoAg==
|
||||
=ww/S
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
# Import
|
||||
|
||||
```bash
|
||||
$ gpg --import pgp/core-devs/*
|
||||
```
|
||||
|
||||
# Verify
|
||||
|
||||
```bash
|
||||
$ gpg --verify pgp/core-devs/than/than-crypto.txt
|
||||
|
||||
|
||||
gpg: Signature made Sat 19 Jul 2025 02:04:10 AM EDT
|
||||
gpg: using EDDSA key 8B3A74890536BAD50D9376EBF1CB32F67E3302A1
|
||||
gpg: Good signature from "nopenothinghere@proton.me <nopenothinghere@proton.me>" [ultimate]
|
||||
gpg: aka "Nope Nothing (Anonymous Planet Contact) <no@anonymousplanet.org>" [ultimate]
|
||||
gpg: aka "Nope Nothing (Systems Administrator) <admin@itsnothing.net>" [ultimate]
|
||||
Primary key fingerprint: 8B3A 7489 0536 BAD5 0D93 76EB F1CB 32F6 7E33 02A1
|
||||
```
|
||||
|
||||
## All signing keys are signed by the Master Signing Key
|
||||
|
||||
TODO
|
||||
# Import
|
||||
|
||||
```sh
|
||||
$ gpg --import pgp/core-devs/*
|
||||
```
|
||||
|
||||
# Verify
|
||||
|
||||
```sh
|
||||
$ gpg --verify pgp/core-devs/than/than-crypto.txt
|
||||
|
||||
|
||||
gpg: Signature made Sat 19 Jul 2025 02:04:10 AM EDT
|
||||
gpg: using EDDSA key 8B3A74890536BAD50D9376EBF1CB32F67E3302A1
|
||||
gpg: Good signature from "nopenothinghere@proton.me <nopenothinghere@proton.me>" [ultimate]
|
||||
gpg: aka "Nope Nothing (Anonymous Planet Contact) <no@anonymousplanet.net>" [ultimate]
|
||||
gpg: aka "Nope Nothing (Systems Administrator) <admin@itsnothing.net>" [ultimate]
|
||||
Primary key fingerprint: 8B3A 7489 0536 BAD5 0D93 76EB F1CB 32F6 7E33 02A1
|
||||
```
|
||||
|
||||
## All signing keys are signed by the Master Signing Key
|
||||
|
||||
TODO
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# scripts/archived
|
||||
|
||||
Scripts kept for reference but no longer part of the active pipeline.
|
||||
|
||||
| Script | Why archived |
|
||||
|--------|-------------|
|
||||
| `tag_release.py` | Created GPG-signed `vX.Y.Z` annotated tags. Superseded by the `release-YYYYMMDD-<sha>` timestamp tagging built into `03-release.yml`. Re-enable if semver release tagging is reintroduced. |
|
||||
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Release tagging helper for Anonymous Planet maintainers.
|
||||
|
||||
Creates a GPG-signed annotated git tag using the release key, with a
|
||||
message derived from the changelog entry for that version.
|
||||
|
||||
Usage:
|
||||
python scripts/tag_release.py # auto-detect next version
|
||||
python scripts/tag_release.py --version v1.2.4
|
||||
python scripts/tag_release.py --version v1.2.4 --key <fingerprint>
|
||||
python scripts/tag_release.py --version v1.2.4 --dry-run
|
||||
|
||||
What it does:
|
||||
1. Checks the working tree is clean and the branch is main
|
||||
2. Resolves the version (auto-increments patch if not given)
|
||||
3. Finds the matching ## [vX.Y.Z] entry in the changelog
|
||||
4. Lists available signing keys and selects the release key
|
||||
5. Creates a signed annotated tag: git tag -s vX.Y.Z -u <key> -m <message>
|
||||
6. Verifies the tag signature
|
||||
7. Prints the push command
|
||||
|
||||
Requirements:
|
||||
- git
|
||||
- GPG with the release signing key imported
|
||||
"""
|
||||
|
||||
# The Hitchhiker's Guide to Online Anonymity © 2026 by Anonymous Planet is licensed under Creative
|
||||
# Commons Attribution-NonCommercial 4.0 International
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Default release signing key fingerprint.
|
||||
# Maintainers with a different key can pass --key on the CLI.
|
||||
DEFAULT_SIGNING_KEY = "C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2"
|
||||
|
||||
CHANGELOG = Path(__file__).resolve().parent.parent / "docs" / "changelog" / "index.md"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(cmd, capture_output=True, text=True, check=check)
|
||||
|
||||
|
||||
def git(*args: str, check: bool = True) -> str:
|
||||
return run(["git", *args], check=check).stdout.strip()
|
||||
|
||||
|
||||
def check_gpg_installed() -> bool:
|
||||
try:
|
||||
return run(["gpg", "--version"], check=False).returncode == 0
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Git state checks
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def check_clean_tree() -> bool:
|
||||
"""Return True if the working tree and index are clean."""
|
||||
status = git("status", "--porcelain")
|
||||
return status == ""
|
||||
|
||||
|
||||
def current_branch() -> str:
|
||||
return git("rev-parse", "--abbrev-ref", "HEAD")
|
||||
|
||||
|
||||
def latest_tag() -> str | None:
|
||||
"""Return the most recent vX.Y.Z tag, or None."""
|
||||
out = run(["git", "tag", "--sort=-version:refname", "--list", "v*"], check=False).stdout
|
||||
for line in out.splitlines():
|
||||
if re.match(r"^v\d+\.\d+\.\d+", line.strip()):
|
||||
return line.strip()
|
||||
return None
|
||||
|
||||
|
||||
def tag_exists(version: str) -> bool:
|
||||
out = run(["git", "tag", "--list", version], check=False).stdout.strip()
|
||||
return bool(out)
|
||||
|
||||
|
||||
def head_sha() -> str:
|
||||
return git("rev-parse", "HEAD")
|
||||
|
||||
|
||||
def short_sha() -> str:
|
||||
return git("rev-parse", "--short", "HEAD")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Version logic
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def auto_increment(tag: str | None) -> str:
|
||||
"""Bump the patch number of tag, or return v1.0.0 if no tag exists."""
|
||||
if not tag:
|
||||
return "v1.0.0"
|
||||
m = re.match(r"^v?(\d+)\.(\d+)\.(\d+)", tag)
|
||||
if not m:
|
||||
return "v1.0.0"
|
||||
return f"v{m.group(1)}.{m.group(2)}.{int(m.group(3)) + 1}"
|
||||
|
||||
|
||||
def normalise_version(v: str) -> str:
|
||||
return v if v.startswith("v") else f"v{v}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Changelog parsing
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def extract_changelog_entry(version: str) -> str | None:
|
||||
"""Return the raw text of the ## [version] section, or None if not found."""
|
||||
if not CHANGELOG.exists():
|
||||
return None
|
||||
|
||||
content = CHANGELOG.read_text(encoding="utf-8")
|
||||
# Find the heading for this version
|
||||
pattern = re.compile(
|
||||
rf"^(## \[{re.escape(version)}\].*?)(?=^## \[|\Z)",
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
m = pattern.search(content)
|
||||
if not m:
|
||||
return None
|
||||
return m.group(1).strip()
|
||||
|
||||
|
||||
def changelog_to_tag_message(version: str, entry: str | None, sha: str) -> str:
|
||||
"""Convert a changelog entry into a clean tag message."""
|
||||
if not entry:
|
||||
return f"{version}\n\nNo changelog entry found for {version}.\nCommit: {sha}"
|
||||
|
||||
lines = []
|
||||
current_bucket = None
|
||||
|
||||
for line in entry.splitlines():
|
||||
# Skip the ## [vX.Y.Z] heading — we'll add our own first line
|
||||
if re.match(r"^## \[", line):
|
||||
continue
|
||||
# Skip admonition headers like !!! Note "Added" → use bucket name as section
|
||||
m = re.match(r'^!!!\s+\w+\s+"(.+)"', line)
|
||||
if m:
|
||||
current_bucket = m.group(1)
|
||||
lines.append(f"\n{current_bucket}:")
|
||||
continue
|
||||
# Skip Meta bucket entirely
|
||||
if current_bucket == "Meta":
|
||||
continue
|
||||
# Strip 4-space admonition indent and leading bullet dash
|
||||
stripped = re.sub(r"^ {4}", "", line)
|
||||
stripped = re.sub(r"^- ", " - ", stripped)
|
||||
# Skip blank lines inside admonitions (keep structure clean)
|
||||
if stripped.strip():
|
||||
lines.append(stripped)
|
||||
|
||||
body = "\n".join(lines).strip()
|
||||
return f"{version}\n\n{body}\n\nCommit: {sha}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# GPG key selection
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def list_secret_keys() -> list[dict]:
|
||||
"""Return a list of available secret keys with fingerprint and uid."""
|
||||
result = run(["gpg", "--list-secret-keys", "--with-colons"], check=False)
|
||||
keys = []
|
||||
current: dict = {}
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split(":")
|
||||
if parts[0] == "sec":
|
||||
if current:
|
||||
keys.append(current)
|
||||
current = {"fingerprint": None, "uid": None, "key_id": parts[4] if len(parts) > 4 else ""}
|
||||
elif parts[0] == "fpr" and current is not None:
|
||||
if not current.get("fingerprint"):
|
||||
current["fingerprint"] = parts[9] if len(parts) > 9 else ""
|
||||
elif parts[0] == "uid" and current is not None:
|
||||
if not current.get("uid"):
|
||||
current["uid"] = parts[9] if len(parts) > 9 else ""
|
||||
if current:
|
||||
keys.append(current)
|
||||
return keys
|
||||
|
||||
|
||||
def resolve_signing_key(preferred: str) -> str | None:
|
||||
"""
|
||||
Return the fingerprint to use for signing.
|
||||
Prefers the key matching `preferred` (full fingerprint or key ID suffix).
|
||||
Falls back to interactive selection if not found.
|
||||
"""
|
||||
keys = list_secret_keys()
|
||||
if not keys:
|
||||
return None
|
||||
|
||||
# Try to match preferred fingerprint/key ID
|
||||
preferred_upper = preferred.upper()
|
||||
for k in keys:
|
||||
fp = (k.get("fingerprint") or "").upper()
|
||||
kid = (k.get("key_id") or "").upper()
|
||||
if preferred_upper in (fp, kid) or fp.endswith(preferred_upper) or kid.endswith(preferred_upper):
|
||||
return k["fingerprint"]
|
||||
|
||||
# Not found — show what's available and ask
|
||||
print("\n⚠ Preferred key not found in keyring. Available keys:\n")
|
||||
for i, k in enumerate(keys, 1):
|
||||
print(f" {i}. {k.get('fingerprint', 'N/A')}")
|
||||
print(f" {k.get('uid', 'Unknown')}\n")
|
||||
|
||||
try:
|
||||
choice = input(f"Select key (1–{len(keys)}) or press Enter to abort: ").strip()
|
||||
if not choice:
|
||||
return None
|
||||
idx = int(choice) - 1
|
||||
if 0 <= idx < len(keys):
|
||||
return keys[idx]["fingerprint"]
|
||||
except (ValueError, EOFError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Tag creation and verification
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def create_signed_tag(version: str, key_fingerprint: str, message: str, dry_run: bool) -> bool:
|
||||
"""Create a GPG-signed annotated tag. Returns True on success."""
|
||||
sha = head_sha()
|
||||
cmd = [
|
||||
"git", "tag",
|
||||
"-s", version,
|
||||
sha,
|
||||
"-u", key_fingerprint,
|
||||
"-m", message,
|
||||
]
|
||||
|
||||
print(f"\n{'─' * 70}")
|
||||
print(f" Tag: {version}")
|
||||
print(f" Commit: {sha[:12]}")
|
||||
print(f" Key: {key_fingerprint}")
|
||||
print(f"{'─' * 70}")
|
||||
print("\nTag message:\n")
|
||||
for line in message.splitlines():
|
||||
print(f" {line}")
|
||||
print()
|
||||
|
||||
if dry_run:
|
||||
print("── DRY RUN — tag not created. Remove --dry-run to proceed.\n")
|
||||
return True
|
||||
|
||||
confirm = input("Create this tag? [y/N] ").strip().lower()
|
||||
if confirm != "y":
|
||||
print("Aborted.")
|
||||
return False
|
||||
|
||||
result = run(cmd, check=False)
|
||||
if result.returncode != 0:
|
||||
print(f"\n✗ Tag creation failed:\n{result.stderr}")
|
||||
return False
|
||||
|
||||
print(f"\n✓ Tag {version} created.")
|
||||
return True
|
||||
|
||||
|
||||
def verify_tag(version: str) -> bool:
|
||||
"""Verify the GPG signature on the tag."""
|
||||
result = run(["git", "tag", "-v", version], check=False)
|
||||
output = result.stdout + result.stderr
|
||||
if result.returncode == 0:
|
||||
print("\n✓ Tag signature verified:")
|
||||
for line in output.splitlines():
|
||||
if any(k in line for k in ("gpg:", "Primary", "using", "issuer", "Good")):
|
||||
print(f" {line.strip()}")
|
||||
return True
|
||||
else:
|
||||
print(f"\n✗ Tag signature verification failed:\n{output}")
|
||||
return False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Main
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Create a GPG-signed release tag from the changelog."
|
||||
)
|
||||
ap.add_argument(
|
||||
"--version", "-v",
|
||||
help="Version to tag, e.g. v1.2.4 (default: auto-increment from latest tag)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--key", "-k",
|
||||
default=DEFAULT_SIGNING_KEY,
|
||||
help=f"GPG key fingerprint to sign with (default: {DEFAULT_SIGNING_KEY})",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--dry-run", "-n",
|
||||
action="store_true",
|
||||
help="Print the tag message and exit without creating the tag",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
root = repo_root()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(" RELEASE TAGGER — Anonymous Planet")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. GPG available?
|
||||
if not check_gpg_installed():
|
||||
print("\n✗ GPG is not installed or not in PATH.")
|
||||
print(" Install with: sudo apt install gnupg | brew install gnupg")
|
||||
return 1
|
||||
|
||||
# 2. Working tree clean?
|
||||
if not check_clean_tree():
|
||||
print("\n✗ Working tree is not clean. Commit or stash changes first.")
|
||||
result = run(["git", "status", "--short"], check=False)
|
||||
print(result.stdout)
|
||||
return 1
|
||||
|
||||
# 3. On main?
|
||||
branch = current_branch()
|
||||
if branch != "main":
|
||||
print(f"\n⚠ You are on branch '{branch}', not 'main'.")
|
||||
confirm = input(" Tag anyway? [y/N] ").strip().lower()
|
||||
if confirm != "y":
|
||||
return 1
|
||||
|
||||
# 4. Resolve version
|
||||
last_tag = latest_tag()
|
||||
version = normalise_version(args.version) if args.version else auto_increment(last_tag)
|
||||
print(f"\n Latest tag: {last_tag or '(none)'}")
|
||||
print(f" New version: {version}")
|
||||
print(f" Branch: {branch}")
|
||||
print(f" HEAD: {short_sha()}")
|
||||
|
||||
if tag_exists(version) and not args.dry_run:
|
||||
print(f"\n✗ Tag {version} already exists.")
|
||||
print(" Use --version to specify a different version, or delete the tag first:")
|
||||
print(f" git tag -d {version}")
|
||||
return 1
|
||||
|
||||
# 5. Build tag message from changelog
|
||||
entry = extract_changelog_entry(version)
|
||||
if not entry:
|
||||
print(f"\n⚠ No changelog entry found for {version}.")
|
||||
print(f" Add a '## [{version}]' section to {CHANGELOG.relative_to(root)}")
|
||||
print(" and re-run, or proceed with a minimal message.")
|
||||
confirm = input(" Proceed with minimal message? [y/N] ").strip().lower()
|
||||
if confirm != "y":
|
||||
return 1
|
||||
|
||||
message = changelog_to_tag_message(version, entry, head_sha())
|
||||
|
||||
# 6. Resolve signing key
|
||||
print(f"\n Signing key: {args.key}")
|
||||
key = resolve_signing_key(args.key)
|
||||
if not key:
|
||||
print("\n✗ Could not resolve a signing key. Aborting.")
|
||||
print(" Import the release key with:")
|
||||
print(" gpg --import pgp/anonymousplanet-release.asc")
|
||||
return 1
|
||||
print(f" Resolved to: {key}")
|
||||
|
||||
# 7. Create tag
|
||||
if not create_signed_tag(version, key, message, args.dry_run):
|
||||
return 1
|
||||
|
||||
if args.dry_run:
|
||||
return 0
|
||||
|
||||
# 8. Verify
|
||||
if not verify_tag(version):
|
||||
return 1
|
||||
|
||||
# 9. Push instructions
|
||||
print("\n" + "=" * 70)
|
||||
print(" ✓ All done. Push the tag with:")
|
||||
print(f"\n git push origin {version}\n")
|
||||
print(" The 03-release.yml workflow can then be triggered manually from")
|
||||
print(" GitHub Actions to publish the GitHub Release for this tag.")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+217
-161
@@ -1,161 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the MkDocs site, then render docs/guide/ to a single PDF via a Chromium-based browser.
|
||||
|
||||
Uses headless Chrome/Edge print-to-PDF (embeds images). WeasyPrint-based mkdocs-with-pdf is
|
||||
omitted here because it needs GTK/Pango (awkward on Windows).
|
||||
|
||||
Usage (from repo root):
|
||||
python scripts/build_guide_pdf.py
|
||||
python scripts/build_guide_pdf.py --site-dir build/html --pdf export/guide.pdf
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def find_chromium_executable() -> Path | None:
|
||||
if sys.platform == "win32":
|
||||
paths = [
|
||||
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
]
|
||||
for p in paths:
|
||||
if p.is_file():
|
||||
return p
|
||||
for name in ("chrome", "msedge"):
|
||||
w = shutil.which(name)
|
||||
if w:
|
||||
return Path(w)
|
||||
elif sys.platform == "darwin":
|
||||
for p in (
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
):
|
||||
if os.path.isfile(p):
|
||||
return Path(p)
|
||||
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"):
|
||||
w = shutil.which(name)
|
||||
if w:
|
||||
return Path(w)
|
||||
return None
|
||||
|
||||
|
||||
def run_mkdocs(site_dir: Path) -> None:
|
||||
site_dir.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "mkdocs", "build", "-d", str(site_dir)],
|
||||
cwd=repo_root(),
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path:
|
||||
"""Write PDF to ``pdf_out``. Uses a temp file first so an open ``guide.pdf`` on Windows
|
||||
does not block the build: if the final path is locked, writes ``guide-new.pdf`` instead.
|
||||
"""
|
||||
pdf_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
partial = pdf_out.parent / f".{pdf_out.name}.writing"
|
||||
partial.unlink(missing_ok=True)
|
||||
|
||||
uri = html_file.resolve().as_uri()
|
||||
# Chromium headless print; allow time for fonts/images on very large pages.
|
||||
cmd = [str(browser)]
|
||||
if os.environ.get("CI"):
|
||||
# GitHub Actions / other CI runners often need these for Chromium to start.
|
||||
cmd += [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
]
|
||||
cmd += [
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--no-pdf-header-footer",
|
||||
f"--print-to-pdf={partial.resolve()}",
|
||||
uri,
|
||||
]
|
||||
subprocess.run(cmd, check=True, timeout=600)
|
||||
deadline = time.time() + 120
|
||||
while time.time() < deadline:
|
||||
if partial.exists() and partial.stat().st_size > 0:
|
||||
break
|
||||
time.sleep(0.25)
|
||||
else:
|
||||
partial.unlink(missing_ok=True)
|
||||
raise RuntimeError(f"PDF was not written to {partial}")
|
||||
|
||||
try:
|
||||
if pdf_out.exists():
|
||||
pdf_out.unlink()
|
||||
except PermissionError:
|
||||
fallback = pdf_out.with_name(f"{pdf_out.stem}-new{pdf_out.suffix}")
|
||||
fallback.unlink(missing_ok=True)
|
||||
partial.replace(fallback)
|
||||
return fallback
|
||||
|
||||
partial.replace(pdf_out)
|
||||
return pdf_out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
ap = argparse.ArgumentParser(description="Build MkDocs + single-page guide PDF.")
|
||||
ap.add_argument(
|
||||
"--site-dir",
|
||||
type=Path,
|
||||
default=root / "site",
|
||||
help="MkDocs output directory (default: ./site)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--pdf",
|
||||
type=Path,
|
||||
default=root / "export" / "guide.pdf",
|
||||
help="Output PDF path (default: ./export/guide.pdf)",
|
||||
)
|
||||
ap.add_argument("--skip-mkdocs", action="store_true", help="Reuse existing site dir; only run print-to-pdf.")
|
||||
args = ap.parse_args()
|
||||
|
||||
guide_html = args.site_dir / "guide" / "index.html"
|
||||
if not args.skip_mkdocs:
|
||||
run_mkdocs(args.site_dir)
|
||||
if not guide_html.is_file():
|
||||
print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
browser = find_chromium_executable()
|
||||
if not browser:
|
||||
print(
|
||||
"No Chromium-based browser found (Chrome, Edge, or Chromium). "
|
||||
"Install Google Chrome or Microsoft Edge, or add Chromium to PATH.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
out = print_to_pdf(browser, guide_html, args.pdf)
|
||||
size_kb = out.stat().st_size // 1024
|
||||
print(f"Wrote {out.resolve()} ({size_kb} KiB)")
|
||||
if out.resolve() != args.pdf.resolve():
|
||||
print(
|
||||
f"Note: {args.pdf.name} was in use; close it and rename or replace with the file above.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
#!/usr/bin/env python3
|
||||
"""Build light-mode PDF with MkDocs + Chromium, then produce dark-mode PDF via convert.py.
|
||||
|
||||
Usage:
|
||||
python scripts/build_guide_pdf.py # Light PDF only
|
||||
python scripts/build_guide_pdf.py --dark # Dark PDF only (requires light PDF to exist)
|
||||
python scripts/build_guide_pdf.py --both # Light PDF, then dark PDF
|
||||
|
||||
Examples:
|
||||
python scripts/build_guide_pdf.py --site-dir build/html --pdf-light export/thgtoa.pdf
|
||||
python scripts/build_guide_pdf.py --both --pdf-light export/thgtoa.pdf --pdf-dark export/thgtoa-dark.pdf
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def find_chromium_executable() -> Path | None:
|
||||
if sys.platform == "win32":
|
||||
paths = [
|
||||
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||
Path(os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe",
|
||||
]
|
||||
for p in paths:
|
||||
if p.is_file():
|
||||
return p
|
||||
for name in ("chrome", "msedge"):
|
||||
w = shutil.which(name)
|
||||
if w:
|
||||
return Path(w)
|
||||
elif sys.platform == "darwin":
|
||||
for p in (
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
):
|
||||
if os.path.isfile(p):
|
||||
return Path(p)
|
||||
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"):
|
||||
w = shutil.which(name)
|
||||
if w:
|
||||
return Path(w)
|
||||
return None
|
||||
|
||||
|
||||
def run_mkdocs(site_dir: Path) -> None:
|
||||
site_dir.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "mkdocs", "build", "-d", str(site_dir)],
|
||||
cwd=repo_root(),
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path:
|
||||
"""Render html_file to pdf_out via headless Chromium.
|
||||
|
||||
Uses a temp file first so an open guide.pdf on Windows does not block the
|
||||
build; if the final path is still locked, writes guide-new.pdf instead.
|
||||
"""
|
||||
pdf_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
partial = pdf_out.parent / f".{pdf_out.name}.writing"
|
||||
partial.unlink(missing_ok=True)
|
||||
|
||||
uri = html_file.resolve().as_uri()
|
||||
|
||||
cmd = [str(browser)]
|
||||
if os.environ.get("CI"):
|
||||
cmd += [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
]
|
||||
cmd += [
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--no-pdf-header-footer",
|
||||
f"--print-to-pdf={partial.resolve()}",
|
||||
uri,
|
||||
]
|
||||
|
||||
subprocess.run(cmd, check=True, timeout=600)
|
||||
|
||||
deadline = time.time() + 120
|
||||
while time.time() < deadline:
|
||||
if partial.exists() and partial.stat().st_size > 0:
|
||||
break
|
||||
time.sleep(0.25)
|
||||
else:
|
||||
partial.unlink(missing_ok=True)
|
||||
raise RuntimeError(f"PDF was not written to {partial}")
|
||||
|
||||
try:
|
||||
if pdf_out.exists():
|
||||
pdf_out.unlink()
|
||||
except PermissionError:
|
||||
fallback = pdf_out.with_name(f"{pdf_out.stem}-new{pdf_out.suffix}")
|
||||
fallback.unlink(missing_ok=True)
|
||||
partial.replace(fallback)
|
||||
return fallback
|
||||
|
||||
partial.replace(pdf_out)
|
||||
return pdf_out
|
||||
|
||||
# Use scripts/convert.py in place of broken dark-mode hack
|
||||
def build_dark_pdf(light_pdf: Path, dark_pdf: Path) -> Path:
|
||||
"""Convert the light PDF to dark mode using scripts/convert.py."""
|
||||
convert_script = repo_root() / "scripts" / "convert.py"
|
||||
print(f"Converting {light_pdf.name} → {dark_pdf.name} (dark mode)…")
|
||||
subprocess.run(
|
||||
[sys.executable, str(convert_script), str(light_pdf), str(dark_pdf)],
|
||||
check=True,
|
||||
)
|
||||
return dark_pdf
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Build MkDocs + single-page guide PDF (light and/or dark mode)."
|
||||
)
|
||||
ap.add_argument(
|
||||
"--site-dir",
|
||||
type=Path,
|
||||
default=root / "build" / "html",
|
||||
help="MkDocs output directory (default: ./build/html)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--pdf-light",
|
||||
type=Path,
|
||||
default=root / "export" / "thgtoa.pdf",
|
||||
help="Output path for light PDF (default: ./export/thgtoa.pdf)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--pdf-dark",
|
||||
type=Path,
|
||||
default=root / "export" / "thgtoa-dark.pdf",
|
||||
help="Output path for dark PDF (default: ./export/thgtoa-dark.pdf)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--skip-mkdocs",
|
||||
action="store_true",
|
||||
help="Reuse existing site dir; skip MkDocs build.",
|
||||
)
|
||||
|
||||
mode = ap.add_mutually_exclusive_group()
|
||||
mode.add_argument("--dark", action="store_true", help="Dark PDF only (light PDF must already exist)")
|
||||
mode.add_argument("--both", action="store_true", help="Build light PDF, then dark PDF")
|
||||
# default (no flag) = light only
|
||||
args = ap.parse_args()
|
||||
|
||||
build_light = not args.dark
|
||||
build_dark = args.dark or args.both
|
||||
|
||||
# --- Light PDF (Chromium) ---
|
||||
if build_light:
|
||||
guide_html = args.site_dir / "guide" / "index.html"
|
||||
|
||||
if not args.skip_mkdocs:
|
||||
run_mkdocs(args.site_dir)
|
||||
|
||||
if not guide_html.is_file():
|
||||
print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
browser = find_chromium_executable()
|
||||
if not browser:
|
||||
print(
|
||||
"No Chromium-based browser found (Chrome, Edge, or Chromium). "
|
||||
"Install Google Chrome or Microsoft Edge, or add Chromium to PATH.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
out_light = print_to_pdf(browser, guide_html, args.pdf_light)
|
||||
size_kb = out_light.stat().st_size // 1024
|
||||
print(f"Wrote {out_light.resolve()} ({size_kb} KiB) [Light Mode]")
|
||||
if out_light.resolve() != args.pdf_light.resolve():
|
||||
print(
|
||||
f"Note: {args.pdf_light.name} was in use; "
|
||||
"close it and rename or replace with the file above.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# --- Dark PDF (pixel converter) ---
|
||||
if build_dark:
|
||||
if not args.pdf_light.exists():
|
||||
print(
|
||||
f"Light PDF not found at {args.pdf_light}. "
|
||||
"Run without --dark first, or use --both.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
out_dark = build_dark_pdf(args.pdf_light, args.pdf_dark)
|
||||
size_kb = out_dark.stat().st_size // 1024
|
||||
print(f"Wrote {out_dark.resolve()} ({size_kb} KiB) [Dark Mode]")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dark-mode PDF converter (pixel-based, batch-safe).
|
||||
|
||||
Rasterizes each page with pdftoppm, applies the hacker theme palette
|
||||
pixel-by-pixel, then reassembles into a PDF. Processes in batches of 50
|
||||
pages to stay within memory limits on large documents, then merges with qpdf.
|
||||
|
||||
Usage:
|
||||
python scripts/convert.py INPUT.pdf [OUTPUT.pdf]
|
||||
python scripts/convert.py INPUT.pdf [OUTPUT.pdf] [--dpi 200]
|
||||
[--bg 1f1f31] [--text e0e0e0] [--link 5e8bde]
|
||||
[--batch-size 50]
|
||||
|
||||
Examples:
|
||||
python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
|
||||
python scripts/convert.py export/thgtoa.pdf --dpi 150 --bg 0d1117
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Defaults (Hacker theme)
|
||||
# --------------------------------------------------------------------------- #
|
||||
DEFAULT_BG = (0x1f, 0x1f, 0x31)
|
||||
DEFAULT_TEXT = (0xe0, 0xe0, 0xe0)
|
||||
DEFAULT_LINK = (0x5e, 0x8b, 0xde)
|
||||
DEFAULT_DPI = 200
|
||||
DEFAULT_BATCH = 50
|
||||
|
||||
|
||||
def hex_to_rgb(h: str) -> tuple:
|
||||
h = h.lstrip('#')
|
||||
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
def apply_dark_theme(
|
||||
img: Image.Image,
|
||||
bg=DEFAULT_BG,
|
||||
text=DEFAULT_TEXT,
|
||||
link=DEFAULT_LINK,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Remap a white-background page image to a dark theme.
|
||||
- Near-white pixels → bg color
|
||||
- Dark pixels (ink/text) → text color
|
||||
- Blue-ish pixels → link color
|
||||
"""
|
||||
arr = np.array(img.convert('RGB'), dtype=np.float32)
|
||||
orig = arr.copy()
|
||||
norm = arr / 255.0
|
||||
|
||||
lightness = (
|
||||
0.299 * norm[:, :, 0]
|
||||
+ 0.587 * norm[:, :, 1]
|
||||
+ 0.114 * norm[:, :, 2]
|
||||
)
|
||||
|
||||
r, g, b = orig[:, :, 0], orig[:, :, 1], orig[:, :, 2]
|
||||
link_mask = (
|
||||
(b > 100)
|
||||
& (b > r * 1.3)
|
||||
& (b > g * 0.9)
|
||||
& (lightness < 0.85)
|
||||
)
|
||||
content_mask = (lightness < 0.85) & ~link_mask
|
||||
blend = ((1.0 - lightness) / 0.85).clip(0, 1)
|
||||
|
||||
bg_f = [c / 255.0 for c in bg]
|
||||
text_f = [c / 255.0 for c in text]
|
||||
link_f = [c / 255.0 for c in link]
|
||||
|
||||
out = np.zeros_like(norm)
|
||||
for i, (b_c, t, lc) in enumerate(zip(bg_f, text_f, link_f)):
|
||||
channel = np.full(lightness.shape, b_c)
|
||||
channel = np.where(content_mask, b_c + blend * (t - b_c), channel)
|
||||
channel = np.where(link_mask, b_c + blend * (lc - b_c), channel)
|
||||
out[:, :, i] = channel
|
||||
|
||||
return Image.fromarray((out * 255).clip(0, 255).astype('uint8'))
|
||||
|
||||
|
||||
def _save_images_as_pdf(images: list, output_path: str) -> None:
|
||||
"""Save a list of RGB PIL images as a PDF without requiring libjpeg.
|
||||
|
||||
Pillow's PDF writer defaults to JPEG encoding for RGB images, which
|
||||
fails when libjpeg is absent in the environment. Fix: quantize each
|
||||
image to palette mode (256 colours, FASTOCTREE) so Pillow uses
|
||||
zlib/deflate instead of JPEG, save each as an individual single-page
|
||||
PDF, then merge the page PDFs with qpdf.
|
||||
|
||||
Colour fidelity is preserved — the hacker theme uses only a handful
|
||||
of distinct colours so 256-colour quantization is visually lossless.
|
||||
"""
|
||||
import tempfile as _tempfile
|
||||
with _tempfile.TemporaryDirectory() as staging:
|
||||
page_pdfs = []
|
||||
for i, img in enumerate(images):
|
||||
page_path = os.path.join(staging, f'p{i:05d}.pdf')
|
||||
img.quantize(colors=256, method=Image.Quantize.FASTOCTREE).save(
|
||||
page_path, format='PDF'
|
||||
)
|
||||
page_pdfs.append(page_path)
|
||||
subprocess.run(
|
||||
['qpdf', '--empty', '--pages'] + page_pdfs + ['--', output_path],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def _check_qpdf() -> bool:
|
||||
return subprocess.run(
|
||||
['qpdf', '--version'], capture_output=True
|
||||
).returncode == 0
|
||||
|
||||
|
||||
def _check_dependencies() -> None:
|
||||
"""Verify required system tools are available before doing any work."""
|
||||
missing = []
|
||||
for tool in ('pdftoppm', 'qpdf'):
|
||||
if subprocess.run(['which', tool], capture_output=True).returncode != 0:
|
||||
missing.append(tool)
|
||||
if missing:
|
||||
tools = ', '.join(missing)
|
||||
instructions = (
|
||||
f"Install with:\n"
|
||||
f" Linux/WSL: sudo apt install poppler-utils qpdf\n"
|
||||
f" macOS: brew install poppler qpdf\n"
|
||||
f" Windows: see docs/code/develop.md"
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Missing required system tool(s): {tools}\n{instructions}"
|
||||
)
|
||||
|
||||
|
||||
def convert_pdf_to_dark(
|
||||
input_path: str | Path,
|
||||
output_path: str | Path,
|
||||
dpi: int = DEFAULT_DPI,
|
||||
bg=DEFAULT_BG,
|
||||
text=DEFAULT_TEXT,
|
||||
link=DEFAULT_LINK,
|
||||
batch_size: int = DEFAULT_BATCH,
|
||||
) -> None:
|
||||
"""
|
||||
Full pipeline: rasterize → apply dark theme → reassemble as PDF.
|
||||
|
||||
For large documents, pages are processed in batches of `batch_size` to
|
||||
avoid OOM, then merged with qpdf. Falls back to single-pass Pillow save
|
||||
if qpdf is not available (fine for small documents).
|
||||
"""
|
||||
input_path = str(input_path)
|
||||
output_path = str(output_path)
|
||||
|
||||
_check_dependencies()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
# 1. Rasterize all pages
|
||||
prefix = os.path.join(tmp, 'page')
|
||||
result = subprocess.run(
|
||||
['pdftoppm', '-r', str(dpi), '-png', input_path, prefix],
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"pdftoppm failed:\n{result.stderr.decode()}"
|
||||
)
|
||||
|
||||
pages = sorted(glob.glob(prefix + '-*.png'))
|
||||
if not pages:
|
||||
raise RuntimeError(
|
||||
"pdftoppm produced no output pages — "
|
||||
"is the PDF valid and not password-protected?"
|
||||
)
|
||||
|
||||
total = len(pages)
|
||||
print(f" Converting {total} page(s) at {dpi} DPI…", flush=True)
|
||||
|
||||
out_dir = os.path.dirname(output_path)
|
||||
if out_dir:
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# 2. Process in batches
|
||||
use_batches = total > batch_size and _check_qpdf()
|
||||
|
||||
if use_batches:
|
||||
batch_dir = os.path.join(tmp, 'batches')
|
||||
os.makedirs(batch_dir)
|
||||
batch_files = []
|
||||
|
||||
for start in range(0, total, batch_size):
|
||||
batch = pages[start:start + batch_size]
|
||||
batch_num = start // batch_size + 1
|
||||
batch_path = os.path.join(batch_dir, f'batch_{batch_num:04d}.pdf')
|
||||
|
||||
print(
|
||||
f" Batch {batch_num}/{(total + batch_size - 1) // batch_size}: "
|
||||
f"pages {start + 1}–{start + len(batch)}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
dark = [apply_dark_theme(Image.open(p), bg, text, link) for p in batch]
|
||||
_save_images_as_pdf(dark, batch_path)
|
||||
batch_files.append(batch_path)
|
||||
del dark
|
||||
|
||||
# 3. Merge batches with qpdf
|
||||
print(" Merging batches…", flush=True)
|
||||
subprocess.run(
|
||||
['qpdf', '--empty', '--pages'] + batch_files + ['--', output_path],
|
||||
check=True,
|
||||
)
|
||||
|
||||
else:
|
||||
# Single-pass for small documents or when qpdf is unavailable
|
||||
dark_pages = []
|
||||
for i, p in enumerate(pages, 1):
|
||||
if i % 50 == 0 or i == 1:
|
||||
print(f" Page {i}/{total}", flush=True)
|
||||
dark_pages.append(apply_dark_theme(Image.open(p), bg, text, link))
|
||||
|
||||
_save_images_as_pdf(dark_pages, output_path)
|
||||
|
||||
size_mb = os.path.getsize(output_path) / 1024 / 1024
|
||||
print(f" Saved → {output_path} ({size_mb:.1f} MB)")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# CLI
|
||||
# --------------------------------------------------------------------------- #
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description='Convert a PDF to dark mode.')
|
||||
parser.add_argument('input', help='Input PDF path')
|
||||
parser.add_argument('output', nargs='?', help='Output PDF path (optional)')
|
||||
parser.add_argument('--dpi', type=int, default=DEFAULT_DPI, help='Rasterization DPI (default: 200)')
|
||||
parser.add_argument('--batch-size', type=int, default=DEFAULT_BATCH, help='Pages per batch (default: 50)')
|
||||
parser.add_argument('--bg', default='1f1f31', help='Background hex color (default: 1f1f31)')
|
||||
parser.add_argument('--text', default='e0e0e0', help='Body text hex color (default: e0e0e0)')
|
||||
parser.add_argument('--link', default='5e8bde', help='Link/blue hex color (default: 5e8bde)')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.output:
|
||||
base = Path(args.input).stem
|
||||
args.output = str(Path(args.input).parent / f"{base}-dark.pdf")
|
||||
|
||||
convert_pdf_to_dark(
|
||||
args.input,
|
||||
args.output,
|
||||
dpi=args.dpi,
|
||||
bg=hex_to_rgb(args.bg),
|
||||
text=hex_to_rgb(args.text),
|
||||
link=hex_to_rgb(args.link),
|
||||
batch_size=args.batch_size,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
COMMIT_MSG_FILE="$1"
|
||||
|
||||
if [ -z "$COMMIT_MSG_FILE" ]; then
|
||||
echo "require-commit-body: no commit message file supplied" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stripped=$(grep -v '^\s*#' "$COMMIT_MSG_FILE" | sed 's/[[:space:]]*$//')
|
||||
|
||||
subject=$(echo "$stripped" | sed -n '1p')
|
||||
separator=$(echo "$stripped" | sed -n '2p')
|
||||
body=$(echo "$stripped" | tail -n +3 | grep -v '^\s*$')
|
||||
|
||||
if [ -z "$subject" ]; then
|
||||
echo ""
|
||||
echo " COMMIT REJECTED: subject line is empty." >&2
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$separator" ] && [ -z "$body" ]; then
|
||||
echo ""
|
||||
echo " COMMIT REJECTED: commit has no body." >&2
|
||||
echo ""
|
||||
echo " Every commit must explain *why*, not just *what*." >&2
|
||||
echo " Format:" >&2
|
||||
echo ""
|
||||
echo " type(scope): short subject" >&2
|
||||
echo ""
|
||||
echo " Explain the motivation for this change. What problem does" >&2
|
||||
echo " it solve? Why is this the right approach? Reference any" >&2
|
||||
echo " relevant issues, prior art, or context." >&2
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$separator" ]; then
|
||||
echo ""
|
||||
echo " COMMIT REJECTED: no blank line between subject and body." >&2
|
||||
echo ""
|
||||
echo " The second line of a commit message must be blank." >&2
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$body" ]; then
|
||||
echo ""
|
||||
echo " COMMIT REJECTED: commit body is empty." >&2
|
||||
echo ""
|
||||
echo " Add at least one line after the blank separator explaining" >&2
|
||||
echo " why this change was made." >&2
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Setup helper for PDF workflow configuration.
|
||||
|
||||
This script helps you configure the necessary GitHub Secrets for the automated
|
||||
PDF build, signing, and VirusTotal scanning workflows.
|
||||
|
||||
Usage:
|
||||
python scripts/setup_workflow.py
|
||||
|
||||
Requirements:
|
||||
- Python 3.8+
|
||||
- GPG installed (for key export)
|
||||
- Access to GitHub repository settings
|
||||
|
||||
What it does:
|
||||
1. Validates your GPG key setup
|
||||
2. Exports the public key for verification
|
||||
3. Provides instructions for adding secrets to GitHub
|
||||
"""
|
||||
|
||||
# The Hitchhiker's Guide to Online Anonymity © 2026 by Anonymous Planet is licensed under Creative
|
||||
# Commons Attribution-NonCommercial 4.0 International
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Computes the root directory of the repository based on the current file’s location
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
# Determines whether the GPG command-line tool is available on the current system
|
||||
def check_gpg_installed() -> bool:
|
||||
"""Check if GPG is installed and accessible."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
|
||||
# Checks whether the GPG tool is installed and accessible on the system
|
||||
# Export public keys, parse, and read selection of keys from the GPG keyring
|
||||
def list_gpg_keys() -> list[dict]:
|
||||
"""List all GPG keys in the keyring."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--list-keys", "--with-colons"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
keys = []
|
||||
current_key = {}
|
||||
|
||||
for line in result.stdout.split('\n'):
|
||||
if line.startswith('pub:'):
|
||||
if current_key:
|
||||
keys.append(current_key)
|
||||
parts = line.split(':')
|
||||
current_key = {
|
||||
'type': parts[1],
|
||||
'key_id': parts[4],
|
||||
'fingerprint': parts[9] if len(parts) > 9 else None,
|
||||
'created': parts[5],
|
||||
'expires': parts[6],
|
||||
'uid': None,
|
||||
}
|
||||
elif line.startswith('uid:'):
|
||||
parts = line.split(':')
|
||||
current_key['uid'] = parts[9] if len(parts) > 9 else None
|
||||
|
||||
if current_key:
|
||||
keys.append(current_key)
|
||||
|
||||
return keys
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error listing GPG keys: {e}")
|
||||
return []
|
||||
|
||||
def export_public_key(key_id: str, output_file: Path | None = None) -> str | None:
|
||||
"""Export a public key in ASCII armor format."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--armor", "--export", key_id],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
if output_file:
|
||||
output_file.write_text(result.stdout)
|
||||
print(f"✓ Public key exported to {output_file}")
|
||||
|
||||
return result.stdout
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error exporting public key: {e}")
|
||||
return None
|
||||
|
||||
# Exporting the given private key in ASCII armor format (requires passphrase)
|
||||
def export_private_key(key_id: str, output_file: Path | None = None) -> str | None:
|
||||
"""Export a private key in ASCII armor format (requires passphrase)."""
|
||||
try:
|
||||
# This will prompt for passphrase interactively
|
||||
result = subprocess.run(
|
||||
["gpg", "--armor", "--export-secret-keys", key_id],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
if output_file:
|
||||
output_file.write_text(result.stdout)
|
||||
print(f"✓ Private key exported to {output_file}")
|
||||
|
||||
return result.stdout
|
||||
# return None if export fails (e.g. wrong passphrase, key not found)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error exporting private key: {e}")
|
||||
return None
|
||||
|
||||
# Validate that the selected GPG key has signing capability
|
||||
def validate_gpg_key(key_id: str) -> bool:
|
||||
"""Validate that a GPG key has signing capability."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--list-keys", "--with-colons", key_id],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Check for 's' (signing) in the pub line
|
||||
for line in result.stdout.split('\n'):
|
||||
if line.startswith('pub:'):
|
||||
flags = line.split(':')[1]
|
||||
return 's' in flags
|
||||
|
||||
return False
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
# Print instructions for configuring GitHub Secrets
|
||||
def print_setup_instructions():
|
||||
"""Print instructions for configuring GitHub Secrets."""
|
||||
print("\n" + "="*70)
|
||||
print("GITHUB SECRETS SETUP INSTRUCTIONS")
|
||||
print("="*70)
|
||||
|
||||
print("""
|
||||
To enable the automated PDF workflow, you need to add three secrets to your
|
||||
GitHub repository:
|
||||
|
||||
1. GPG_PRIVATE_KEY
|
||||
- Your GPG private key in ASCII armor format
|
||||
- Used to sign PDFs and hash files
|
||||
- IMPORTANT: Keep this secret! Never commit it publicly
|
||||
|
||||
2. GPG_PASSPHRASE
|
||||
- The passphrase for your GPG private key
|
||||
- Required to unlock the private key for signing
|
||||
|
||||
3. VT_API_KEY (optional but recommended)
|
||||
- VirusTotal API key for malware scanning
|
||||
|
||||
TROUBLESHOOTING:
|
||||
|
||||
- If GPG signing fails: Check that your key has signing capability ('s' flag)
|
||||
- If passphrase is wrong: Verify you're using the correct passphrase
|
||||
- If VT scan fails: Ensure API key is valid and within rate limits
|
||||
""")
|
||||
|
||||
def main() -> int:
|
||||
print("\n" + "="*70)
|
||||
print("PDF WORKFLOW SETUP HELPER")
|
||||
print("="*70)
|
||||
|
||||
# Check GPG installation
|
||||
if not check_gpg_installed():
|
||||
print("⚠ WARNING: GPG is not installed or not in PATH")
|
||||
print("Please install GPG before continuing:")
|
||||
print(" - Linux: sudo apt install gnupg")
|
||||
print("\nContinuing anyway...")
|
||||
|
||||
# List available keys
|
||||
print("\n🔑 Available GPG Keys:")
|
||||
print("-" * 70)
|
||||
|
||||
keys = list_gpg_keys()
|
||||
|
||||
if not keys:
|
||||
print("No GPG keys found in your keyring.")
|
||||
print("Generate a key with: gpg --full-generate-key")
|
||||
return 1
|
||||
|
||||
for i, key in enumerate(keys, 1):
|
||||
status = "✓" if validate_gpg_key(key['key_id']) else "✗"
|
||||
print(f"\n{i}. {status} Key ID: {key['key_id']}")
|
||||
print(f" Fingerprint: {key.get('fingerprint', 'N/A')}")
|
||||
print(f" UID: {key.get('uid', 'Unknown')}")
|
||||
print(f" Created: {key.get('created', 'Unknown')}")
|
||||
|
||||
if key.get('expires'):
|
||||
print(f" Expires: {key['expires']}")
|
||||
|
||||
# Ask user to select key
|
||||
print("\n" + "-" * 70)
|
||||
try:
|
||||
choice = input("\nEnter the number of the key you want to use (1-{}): ".format(len(keys)))
|
||||
selected_index = int(choice) - 1
|
||||
|
||||
if not (0 <= selected_index < len(keys)):
|
||||
print("Invalid selection!")
|
||||
return 1
|
||||
|
||||
except ValueError:
|
||||
print("Invalid input! Please enter a number.")
|
||||
return 1
|
||||
|
||||
selected_key = keys[selected_index]
|
||||
|
||||
# Validate key has signing capability
|
||||
if not validate_gpg_key(selected_key['key_id']):
|
||||
print(f"\n⚠ WARNING: Selected key does not have signing capability!")
|
||||
print("You need a key with 's' (signing) flag for PDF signatures.")
|
||||
confirm = input("Continue anyway? (y/N): ")
|
||||
if confirm.lower() != 'y':
|
||||
return 1
|
||||
|
||||
# Export public key
|
||||
print(f"\n📤 Exporting public key for {selected_key['uid']}...")
|
||||
public_key_file = repo_root() / "pgp" / "workflow-public.asc"
|
||||
|
||||
public_key = export_public_key(selected_key['key_id'], public_key_file)
|
||||
|
||||
if not public_key:
|
||||
print("Failed to export public key!")
|
||||
return 1
|
||||
|
||||
# Show public key info
|
||||
print("\n✓ Public Key Information:")
|
||||
print("-" * 70)
|
||||
for line in public_key.split('\n')[:5]:
|
||||
print(line)
|
||||
print("...")
|
||||
|
||||
# Instructions for private key export
|
||||
print("\n🔐 Private Key Export:")
|
||||
print("-" * 70)
|
||||
print("""
|
||||
To get your private key for the GPG_PRIVATE_KEY secret:
|
||||
|
||||
1. Run this command (you'll be prompted for passphrase):
|
||||
gpg --armor --export-secret-keys {} > workflow-private.asc
|
||||
|
||||
2. Copy the ENTIRE output including BEGIN and END lines
|
||||
|
||||
3. Add it to GitHub Secrets as 'GPG_PRIVATE_KEY'
|
||||
|
||||
⚠ IMPORTANT: Keep your private key secure! Never commit it publicly.
|
||||
""".format(selected_key['key_id']))
|
||||
|
||||
# Print setup instructions
|
||||
print_setup_instructions()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("SETUP COMPLETE!")
|
||||
print("="*70)
|
||||
print(f"\nPublic key saved to: {public_key_file}")
|
||||
print("Next steps:")
|
||||
print("1. Export your private key (see instructions above)")
|
||||
print("2. Add all three secrets to GitHub repository settings")
|
||||
print("3. Test the workflow by triggering a manual build")
|
||||
print("\nFor more information, see: docs/guide/dev-workflow.md\n")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Auto-generate and prepend a changelog entry to docs/changelog/index.md.
|
||||
|
||||
Called by .github/workflows/04-changelog.yml. Reads git log since the last
|
||||
changelog version, categorises commits by conventional-commit prefix,
|
||||
and prepends a new ## [vX.Y.Z] section in the MkDocs admonition format used
|
||||
by the rest of the file.
|
||||
|
||||
Environment variables:
|
||||
MANUAL_VERSION Version string to record (required when run from CI).
|
||||
Falls back to auto-increment from the changelog for local runs.
|
||||
TRIGGERING_SHA The commit SHA that triggered this run (used as range end).
|
||||
DRY_RUN If "true", print the entry and exit without writing.
|
||||
|
||||
Note: version is sourced from the changelog file, not from git tags. Git tags
|
||||
are no longer used as the version authority. The changelog is the source of truth.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CHANGELOG = Path(__file__).resolve().parent.parent / "docs" / "changelog" / "index.md"
|
||||
|
||||
# Conventional-commit prefixes → changelog bucket
|
||||
BUCKET_MAP: dict[str, str] = {
|
||||
"feat": "Added",
|
||||
"feature": "Added",
|
||||
"add": "Added",
|
||||
"fix": "Fixed",
|
||||
"bugfix": "Fixed",
|
||||
"perf": "Changed",
|
||||
"refactor": "Changed",
|
||||
"change": "Changed",
|
||||
"chore": "Changed",
|
||||
"ci": "Changed",
|
||||
"docs": "Changed",
|
||||
"style": "Changed",
|
||||
"test": "Changed",
|
||||
"revert": "Fixed",
|
||||
"security": "Fixed",
|
||||
"build": "Changed",
|
||||
}
|
||||
|
||||
BUCKET_ORDER = ["Added", "Changed", "Fixed"]
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> str:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def auto_increment_version(current: str | None) -> str:
|
||||
"""Bump the patch number of the current version, or default to v1.0.0."""
|
||||
if not current:
|
||||
return "v1.0.0"
|
||||
m = re.match(r"^v?(\d+)\.(\d+)\.(\d+)", current)
|
||||
if not m:
|
||||
return "v1.0.0"
|
||||
major, minor, patch = int(m.group(1)), int(m.group(2)), int(m.group(3))
|
||||
return f"v{major}.{minor}.{patch + 1}"
|
||||
|
||||
|
||||
def version_from_changelog() -> str | None:
|
||||
"""Parse the most recent ## [vX.Y.Z] heading from the changelog file.
|
||||
|
||||
This is the primary version source — the changelog is the authority,
|
||||
not git tags.
|
||||
"""
|
||||
if not CHANGELOG.exists():
|
||||
return None
|
||||
for line in CHANGELOG.read_text(encoding="utf-8").splitlines():
|
||||
m = re.match(r"^## \[(v\d+\.\d+\.\d+)\]", line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def commits_since(ref: str | None, until: str) -> list[str]:
|
||||
"""Return one-line commit messages between ref and until (exclusive/inclusive).
|
||||
|
||||
When no ref is given we fall back to the merge-base between HEAD and
|
||||
origin/main to avoid dumping the entire history into the changelog.
|
||||
"""
|
||||
if ref:
|
||||
log_range = f"{ref}..{until}"
|
||||
else:
|
||||
merge_base = run(["git", "merge-base", "HEAD", "origin/main"])
|
||||
if merge_base:
|
||||
log_range = f"{merge_base}..{until}"
|
||||
else:
|
||||
# Truly brand new repo with no remote — limit to last 50 commits
|
||||
log_range = f"-50 {until}"
|
||||
out = run(["git", "log", "--pretty=format:%s", log_range])
|
||||
return [line.strip() for line in out.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def categorise(messages: list[str]) -> dict[str, list[str]]:
|
||||
"""Sort commit messages into Added / Changed / Fixed buckets."""
|
||||
buckets: dict[str, list[str]] = {b: [] for b in BUCKET_ORDER}
|
||||
|
||||
NOISE = re.compile(
|
||||
r"""
|
||||
\[skip\ ci\] # CI skip marker
|
||||
| ^Merge\ (pull\ request|branch) # merge commits
|
||||
| ^chore:\ bump # version bump chores
|
||||
| update\ changelog # self-referential
|
||||
| ^\d+/\d+ # numbered commit series (e.g. 3/8)
|
||||
| ^Tweaking # vague WIP messages
|
||||
| ^Moving\ some # vague WIP messages
|
||||
| \ pt\d+$ # "...pt2", "...pt3" suffixes
|
||||
| ^Fix\ (workflow|path|README)$ # one-word infrastructure fixes
|
||||
| ^Still\ broken # embarrassing mid-fix notes
|
||||
| ^WIP\b # work in progress
|
||||
| ^Forgot\ to # oops commits
|
||||
| ^Revert\ " # reverts (surface the original instead)
|
||||
| ^One\ job\ to\ rule # joke commit messages
|
||||
""",
|
||||
re.VERBOSE | re.IGNORECASE,
|
||||
)
|
||||
|
||||
for msg in messages:
|
||||
if NOISE.search(msg):
|
||||
continue
|
||||
|
||||
m = re.match(r"^(\w+)(?:\([^)]+\))?!?:\s*(.+)$", msg)
|
||||
if m:
|
||||
prefix = m.group(1).lower()
|
||||
description = m.group(2).strip()
|
||||
bucket = BUCKET_MAP.get(prefix, "Changed")
|
||||
else:
|
||||
description = msg
|
||||
bucket = "Changed"
|
||||
|
||||
description = description[0].upper() + description[1:] if description else description
|
||||
buckets[bucket].append(description)
|
||||
|
||||
return buckets
|
||||
|
||||
|
||||
def format_admonition(bucket: str, items: list[str]) -> str:
|
||||
lines = [f'!!! Note "{bucket}"', ""]
|
||||
for item in items:
|
||||
lines.append(f" - {item}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_entry(version: str, buckets: dict[str, list[str]], sha: str) -> str:
|
||||
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
lines = [f"## [{version}]", ""]
|
||||
lines.append('!!! Note "Meta"')
|
||||
lines.append("")
|
||||
lines.append(f" - Released {date} from [`{sha[:7]}`](https://github.com/Anon-Planet/thgtoa/commit/{sha})")
|
||||
lines.append("")
|
||||
|
||||
for bucket in BUCKET_ORDER:
|
||||
if buckets.get(bucket):
|
||||
lines.append(format_admonition(bucket, buckets[bucket]))
|
||||
|
||||
if not any(buckets[b] for b in BUCKET_ORDER):
|
||||
lines.append('!!! Note "Changed"')
|
||||
lines.append("")
|
||||
lines.append(" - Minor updates and maintenance")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def prepend_entry(entry: str) -> None:
|
||||
"""Insert the new entry after the # Release Notes heading."""
|
||||
content = CHANGELOG.read_text(encoding="utf-8")
|
||||
|
||||
insert_at = content.find("\n## ")
|
||||
if insert_at == -1:
|
||||
content = content.rstrip() + "\n\n" + entry + "\n"
|
||||
else:
|
||||
content = content[: insert_at + 1] + entry + "\n" + content[insert_at + 1 :]
|
||||
|
||||
CHANGELOG.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def already_has_version(version: str) -> bool:
|
||||
if not CHANGELOG.exists():
|
||||
return False
|
||||
return f"## [{version}]" in CHANGELOG.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
dry_run = os.environ.get("DRY_RUN", "false").lower() == "true"
|
||||
manual_version = os.environ.get("MANUAL_VERSION", "").strip()
|
||||
triggering_sha = os.environ.get("TRIGGERING_SHA", "HEAD").strip() or "HEAD"
|
||||
|
||||
# Version authority: MANUAL_VERSION (required in CI) → changelog → auto-increment.
|
||||
# Git tags are intentionally not consulted.
|
||||
last_cl_ver = version_from_changelog()
|
||||
new_version = manual_version or auto_increment_version(last_cl_ver)
|
||||
|
||||
print(f"Last CL version: {last_cl_ver or '(none)'}")
|
||||
print(f"New version: {new_version}")
|
||||
print(f"Triggering SHA: {triggering_sha}")
|
||||
|
||||
if already_has_version(new_version) and not manual_version:
|
||||
print(f"Changelog already contains {new_version} — nothing to do.")
|
||||
return 0
|
||||
|
||||
if already_has_version(new_version) and manual_version:
|
||||
print(f"::error::Changelog already contains {new_version}. Choose a different version.")
|
||||
return 1
|
||||
|
||||
# Collect commits since the last changelog version (using it as a git ref
|
||||
# is a best-effort — if the tag doesn't exist, commits_since handles it gracefully).
|
||||
messages = commits_since(last_cl_ver, triggering_sha)
|
||||
print(f"Commits found: {len(messages)}")
|
||||
for m in messages:
|
||||
print(f" {m}")
|
||||
|
||||
buckets = categorise(messages)
|
||||
entry = build_entry(new_version, buckets, triggering_sha)
|
||||
|
||||
print("\n--- Generated entry ---")
|
||||
print(entry)
|
||||
print("-----------------------\n")
|
||||
|
||||
if dry_run:
|
||||
print("DRY RUN — not writing to file.")
|
||||
return 0
|
||||
|
||||
prepend_entry(entry)
|
||||
print(f"Prepended {new_version} to {CHANGELOG}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verification script for thgtoa PDF releases.
|
||||
|
||||
Verifies SHA-256 hashes, BLAKE2b hashes, and GPG signatures (.asc) for
|
||||
the light and dark PDFs. Optionally checks VirusTotal scan status.
|
||||
|
||||
Usage:
|
||||
python scripts/verify_pdf.py
|
||||
python scripts/verify_pdf.py --hashes
|
||||
python scripts/verify_pdf.py --signatures
|
||||
python scripts/verify_pdf.py --vt
|
||||
python scripts/verify_pdf.py --file export/thgtoa.pdf --hashes
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _read_bare_hash(hash_file: Path) -> str | None:
|
||||
"""Read a bare hex digest from a single-value hash file."""
|
||||
try:
|
||||
return hash_file.read_text(encoding="utf-8").strip().split()[0]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def _read_hash_from_sumfile(sum_file: Path, pdf_path: Path) -> str | None:
|
||||
"""Read a hash from a two-column sumfile (sha256sum / b2sum format).
|
||||
|
||||
Matches on the filename only (not the full path) so the file can be used
|
||||
regardless of where the PDFs sit on disk.
|
||||
"""
|
||||
if not sum_file.exists():
|
||||
return None
|
||||
target = pdf_path.name
|
||||
try:
|
||||
for line in sum_file.read_text(encoding="utf-8").splitlines():
|
||||
parts = line.strip().split(None, 1)
|
||||
if len(parts) == 2 and Path(parts[1].lstrip("*")).name == target:
|
||||
return parts[0]
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# Hash verification
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _blake2b(path: Path) -> str:
|
||||
h = hashlib.blake2b()
|
||||
with path.open("rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def verify_hashes(pdf: Path, export_dir: Path) -> bool:
|
||||
"""Verify all available hash files for a PDF. Returns True if all pass."""
|
||||
stem = pdf.name # e.g. "thgtoa.pdf" or "thgtoa-dark.pdf"
|
||||
results: list[bool] = []
|
||||
|
||||
checks = [
|
||||
("SHA-256", _sha256, export_dir / f"{stem}.sha256", export_dir / "sha256sums.txt"),
|
||||
("BLAKE2b", _blake2b, export_dir / f"{stem}.b2sum", export_dir / "b2sums.txt"),
|
||||
]
|
||||
|
||||
for algo, fn, bare_file, sum_file in checks:
|
||||
# Resolve expected hash — prefer bare file, fall back to sumfile
|
||||
expected = _read_bare_hash(bare_file) if bare_file.exists() else None
|
||||
if expected is None:
|
||||
expected = _read_hash_from_sumfile(sum_file, pdf)
|
||||
if expected is None:
|
||||
print(f" ⚠ {algo}: no hash file found (checked {bare_file.name}, {sum_file.name})")
|
||||
continue
|
||||
|
||||
actual = fn(pdf)
|
||||
ok = actual == expected
|
||||
results.append(ok)
|
||||
mark = "✓" if ok else "✗"
|
||||
print(f" {mark} {algo}")
|
||||
if not ok:
|
||||
print(f" expected: {expected}")
|
||||
print(f" actual: {actual}")
|
||||
|
||||
return all(results) if results else False
|
||||
|
||||
# Signature verification
|
||||
|
||||
def verify_signature(pdf: Path) -> bool | None:
|
||||
"""Verify the .asc detached signature for a PDF.
|
||||
|
||||
Returns True on success, False on failure, None if GPG is not installed
|
||||
or the signature file is missing.
|
||||
"""
|
||||
sig = pdf.with_suffix(pdf.suffix + ".asc")
|
||||
if not sig.exists():
|
||||
print(f" ⚠ Signature file not found: {sig.name}")
|
||||
return None
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--verify", str(sig), str(pdf)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
print(" ⚠ GPG not installed — skipping signature verification")
|
||||
return None
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f" ✓ GPG signature valid")
|
||||
# Surface the key info line from stderr (that's where gpg writes it)
|
||||
for line in result.stderr.splitlines():
|
||||
if any(kw in line for kw in ("Good signature", "key ID", "fingerprint", "using")):
|
||||
print(f" {line.strip()}")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ GPG signature INVALID")
|
||||
for line in result.stderr.splitlines():
|
||||
if line.strip():
|
||||
print(f" {line.strip()}")
|
||||
return False
|
||||
|
||||
# VirusTotal
|
||||
|
||||
def check_virustotal(pdf: Path, api_key: str) -> bool:
|
||||
"""Query VirusTotal for the SHA-256 of a PDF. Returns True if clean."""
|
||||
file_hash = _sha256(pdf)
|
||||
url = f"https://www.virustotal.com/api/v3/files/{file_hash}"
|
||||
req = urllib.request.Request(url, headers={"x-apikey": api_key})
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
print(f" ⚠ Not yet scanned on VirusTotal (hash: {file_hash[:16]}…)")
|
||||
else:
|
||||
print(f" ⚠ VirusTotal HTTP error: {e.code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" ⚠ VirusTotal error: {e}")
|
||||
return False
|
||||
|
||||
stats = data.get("data", {}).get("attributes", {}).get("last_analysis_stats", {})
|
||||
malicious = stats.get("malicious", 0)
|
||||
suspicious = stats.get("suspicious", 0)
|
||||
undetected = stats.get("undetected", 0)
|
||||
harmless = stats.get("harmless", 0)
|
||||
total = malicious + suspicious + undetected + harmless
|
||||
|
||||
clean = malicious == 0 and suspicious == 0
|
||||
mark = "✓" if clean else "✗"
|
||||
print(f" {mark} VirusTotal ({malicious} malicious, {suspicious} suspicious, "
|
||||
f"{harmless} clean / {total} engines)")
|
||||
print(f" https://www.virustotal.com/gui/file/{file_hash}")
|
||||
return clean
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
export = root / "export"
|
||||
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Verify thgtoa PDF hashes, signatures, and VirusTotal status.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
ap.add_argument(
|
||||
"--file",
|
||||
type=Path,
|
||||
default=None,
|
||||
metavar="PDF",
|
||||
help="Verify a single PDF instead of both light and dark",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--export-dir",
|
||||
type=Path,
|
||||
default=export,
|
||||
metavar="DIR",
|
||||
help=f"Directory containing hash and signature files (default: {export})",
|
||||
)
|
||||
ap.add_argument("--hashes", action="store_true", help="Verify hashes only")
|
||||
ap.add_argument("--signatures", action="store_true", help="Verify signatures only")
|
||||
ap.add_argument("--vt", action="store_true", help="Check VirusTotal status")
|
||||
args = ap.parse_args()
|
||||
|
||||
# Default: verify everything
|
||||
do_hashes = args.hashes or not any([args.hashes, args.signatures, args.vt])
|
||||
do_sigs = args.signatures or not any([args.hashes, args.signatures, args.vt])
|
||||
do_vt = args.vt or not any([args.hashes, args.signatures, args.vt])
|
||||
|
||||
# Resolve PDFs to check
|
||||
if args.file:
|
||||
pdfs = [args.file]
|
||||
else:
|
||||
pdfs = [export / "thgtoa.pdf", export / "thgtoa-dark.pdf"]
|
||||
|
||||
vt_api_key = os.environ.get("VT_API_KEY", "")
|
||||
|
||||
overall_pass = True
|
||||
|
||||
for pdf in pdfs:
|
||||
bar = "─" * 60
|
||||
print(f"\n{bar}")
|
||||
print(f" {pdf.name}")
|
||||
print(bar)
|
||||
|
||||
if not pdf.exists():
|
||||
print(f" ⚠ File not found: {pdf} — skipping")
|
||||
overall_pass = False
|
||||
continue
|
||||
|
||||
if do_hashes:
|
||||
ok = verify_hashes(pdf, args.export_dir)
|
||||
if not ok:
|
||||
overall_pass = False
|
||||
|
||||
if do_sigs:
|
||||
result = verify_signature(pdf)
|
||||
if result is False:
|
||||
overall_pass = False
|
||||
|
||||
if do_vt:
|
||||
if not vt_api_key:
|
||||
print(" ⚠ VT_API_KEY not set — skipping VirusTotal check")
|
||||
else:
|
||||
ok = check_virustotal(pdf, vt_api_key)
|
||||
if not ok:
|
||||
overall_pass = False
|
||||
|
||||
print(f"\n{'─' * 60}")
|
||||
if overall_pass:
|
||||
print(" ✓ All checks passed")
|
||||
else:
|
||||
print(" ✗ One or more checks failed")
|
||||
print()
|
||||
|
||||
return 0 if overall_pass else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user