mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:37:03 +01:00
The leak this repository had was not carelessness with a secret. It was somebody reaching for a real name because it was the example in front of them -- a fixture built from a live database, an envelope pasted out of a running container, a help text written while looking at the actual group. Three different people-shaped mistakes, none of which a rule about "be careful" would have stopped. So it is a gate. The hard part is that the thing being checked must not contain the list of what to refuse, or the check *is* the leak. It reads the names at run time from outside: the photo pack's manifest, the message database, the local .env. It never prints the word it matched, because this output reaches CI logs. On a machine with none of that private data it says "shapes only" out loud rather than passing quietly, and CI enforces the half that needs nothing -- phone numbers, uuids and Signal group ids, compared against an allowlist of values invented for the fixtures. It runs on staged files, on the commit message -- where two of the leaks actually travelled -- and over the whole tree in CI. It paid for itself twice before it was wired up. The live test group's id was in tests/test_config.py in two spellings, and truncated in three more files, after four rounds of grepping by hand had pronounced the tree clean -- nobody thinks to grep for base64. Then it refused the commit that introduced it, because DOCS-privacy.md had used a real name as the example of a name. An allowlist line may be scoped to one file, which is how the given names that are also popes stay legal in popes.json and refused in every fixture that has no business carrying them. The threshold is three characters, because one of this group's given names is three characters long. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
84 lines
4.0 KiB
Markdown
84 lines
4.0 KiB
Markdown
# What is private, and what stops it leaking
|
|
|
|
This repository is public. The bot it describes reads a private group chat,
|
|
posts from a private photo album, and keeps a database of who said how much.
|
|
None of that may be in here, and on 2026-08-30 some of it was.
|
|
|
|
## What leaked, and how
|
|
|
|
Real first names, one full name, two phone numbers and a group member's
|
|
Signal uuid were in the tree *and* in the public history. They arrived one
|
|
way every time: **somebody reached for a real name because it was the example
|
|
in front of them** — a test fixture built from a live database dump, an
|
|
envelope pasted verbatim out of a running container, a help text written
|
|
while looking at the actual group.
|
|
|
|
Fixing it needed the tree scrubbed, all 56 commits rewritten with
|
|
`git filter-repo`, and then the repository **deleted and recreated**, because
|
|
GitHub keeps orphaned commits fetchable by their SHA after a force-push and
|
|
only the repository going away takes them with it. The closed pull requests
|
|
and the star went too.
|
|
|
|
## What is private, and where it lives
|
|
|
|
| | Where | Never in git because |
|
|
|---|---|---|
|
|
| the photos and who is in them | `~/.local/share/signal-bot/pack/` | it is outside the repo, and `.gitignore` also names `people.json`, `tags.json`, `media/`, `pack/` and every media extension |
|
|
| the message database | `signal_bot.sqlite3` | `*.sqlite3` and its `-wal`/`-shm` siblings are ignored |
|
|
| the account's number, groups, inviters | `.env` | `.env` is ignored; `.env.sample` carries the shape and no values |
|
|
| everyone's names | the database and the pack manifest | nothing reads them into the repo; the bot logs `pack:p1`, never a name |
|
|
|
|
All of it is `700`/`600` on disk. The repository never needs any of it: the
|
|
tests, the documents and the offline end-to-end run use invented people —
|
|
alfa, beta, gamma, ala, basia, celina — and invented ids that base64-decode
|
|
twice into a sentence saying what they are.
|
|
|
|
## The gate
|
|
|
|
`scripts/check_personal_data.sh` refuses a commit that carries personal data.
|
|
It runs on staged files, on the commit message, and over the whole tree in
|
|
CI. Two halves, because they fail differently:
|
|
|
|
- **Names cannot be listed here** — a file of real names kept in the repo to
|
|
check against *is* the leak. So the checker learns them at run time from
|
|
outside: the photo pack's manifest, the message database, and `.env`. It
|
|
never prints the word it matched, because this output reaches CI logs.
|
|
- **Shapes can be listed** — a phone number, a uuid and a Signal group id
|
|
look like themselves anywhere, so those are matched by pattern and compared
|
|
against `.personal-data-allowlist`, which holds only invented values.
|
|
|
|
On a machine with none of that private data (CI, a fresh clone) the name half
|
|
has nothing to compare against and **says so** rather than passing quietly.
|
|
|
|
It found a real leak the first time it was run: the live test group's id, in
|
|
two spellings, in `tests/test_config.py` — which four rounds of grepping by
|
|
hand had missed, because nobody thinks to grep for base64.
|
|
|
|
## When it fires
|
|
|
|
```
|
|
tests/test_thing.py:12: a name from your private data
|
|
```
|
|
|
|
Use an invented name. `ala`, `basia`, `celina`, `darek` are already in use;
|
|
so are `+15550000000` and ids that decode to `e2e-watched-group-id-32-bytes!!!`.
|
|
|
|
```
|
|
DOCS-thing.md:40: phone number '+48123456789' is not in .personal-data-allowlist
|
|
```
|
|
|
|
Either it belongs to somebody — replace it — or it is invented, in which case
|
|
add it to `.personal-data-allowlist` **after checking that it is**. A line
|
|
there may be scoped to one file (`signal_bot/popes.json::hadrian`), which is how
|
|
a given name that is also a pope stays refused in every test fixture that has
|
|
no business carrying it.
|
|
|
|
## What the gate cannot do
|
|
|
|
- It does not scan history. Rewriting that is a deliberate, separate act.
|
|
- It knows only what is on *this* machine. A name nobody has ever typed into
|
|
the group is a name it cannot recognise.
|
|
- It does not stop the bot's own logs holding display names —
|
|
`journalctl --user -u signal-bot` does, and `--vacuum-time=` is how that is
|
|
emptied.
|