## Summary
Adds `corescope-decrypt` — a standalone CLI tool that decrypts and
exports MeshCore hashtag channel messages from a CoreScope SQLite
database.
### What it does
MeshCore hashtag channels use symmetric encryption with keys derived
from the channel name. The CoreScope ingestor stores **all** GRP_TXT
packets, even those it can't decrypt. This tool enables retroactive
decryption — decrypt historical messages for any channel whose name you
learn after the fact.
### Architecture
- **`internal/channel/`** — Shared crypto package extracted from
ingestor logic:
- `DeriveKey()` — `SHA-256("#name")[:16]`
- `ChannelHash()` — 1-byte packet filter (`SHA-256(key)[0]`)
- `Decrypt()` — HMAC-SHA256 MAC verify + AES-128-ECB
- `ParsePlaintext()` — timestamp + flags + "sender: message" parsing
- **`cmd/decrypt/`** — CLI binary with three output formats:
- `--format json` — Full metadata (observers, path, raw hex)
- `--format html` — Self-contained interactive viewer with search/sort
- `--format irc` (or `log`) — Plain-text IRC-style log, greppable
### Usage
```bash
# JSON export
corescope-decrypt --channel "#wardriving" --db meshcore.db
# Interactive HTML viewer
corescope-decrypt --channel wardriving --db meshcore.db --format html --output wardriving.html
# Greppable log
corescope-decrypt --channel "#wardriving" --db meshcore.db --format irc | grep "KE6QR"
# From Docker
docker exec corescope-prod /app/corescope-decrypt --channel "#wardriving" --db /app/data/meshcore.db
```
### Build & deployment
- Statically linked (`CGO_ENABLED=0`) — zero dependencies
- Added to Dockerfile (available at `/app/corescope-decrypt` in
container)
- CI: builds and tests in go-test job
- CI: attaches linux/amd64 and linux/arm64 binaries to GitHub Releases
on tags
### Testing
- `internal/channel/` — 9 tests: key derivation, encrypt/decrypt
round-trip, MAC rejection, wrong-channel rejection, plaintext parsing
- `cmd/decrypt/` — 7 tests: payload extraction, channel hash
consistency, all 3 output formats, JSON parseability, fixture DB
integration
- Verified against real fixture DB: successfully decrypts 17
`#wardriving` messages
### Limitations
- Hashtag channels only (name-derived keys). Custom PSK channels not
supported.
- No DM decryption (asymmetric, per-peer keys).
- Read-only database access.
Fixes #723
---------
Co-authored-by: you <you@example.com>
corescope-decrypt
Standalone CLI tool to decrypt and export MeshCore hashtag channel messages from a CoreScope SQLite database.
Why
MeshCore hashtag channels use symmetric encryption where the key is derived deterministically from the channel name. The CoreScope ingestor stores all GRP_TXT packets in the database, including those it cannot decrypt at ingest time.
This tool enables:
- Retroactive decryption — decrypt historical messages for any channel whose name you learn after the fact
- Forensics & analysis — export channel traffic for offline review
- Bulk export — dump an entire channel's history as JSON, HTML, or plain text
Installation
From Docker image
The binary is included in the CoreScope Docker image at /app/corescope-decrypt:
docker exec corescope-prod /app/corescope-decrypt --channel "#wardriving" --db /app/data/meshcore.db
From GitHub release
Download the static binary from the Releases page:
# Linux amd64
curl -LO https://github.com/Kpa-clawbot/CoreScope/releases/latest/download/corescope-decrypt-linux-amd64
chmod +x corescope-decrypt-linux-amd64
./corescope-decrypt-linux-amd64 --help
Build from source
cd cmd/decrypt
CGO_ENABLED=0 go build -ldflags="-s -w" -o corescope-decrypt .
The binary is statically linked — no dependencies, runs on any Linux.
Usage
corescope-decrypt --channel NAME --db PATH [--format FORMAT] [--output FILE]
Run corescope-decrypt --help for full flag documentation.
JSON output (default)
Machine-readable, includes all metadata (observers, path hops, raw hex):
corescope-decrypt --channel "#wardriving" --db meshcore.db
[
{
"hash": "a1b2c3...",
"timestamp": "2026-04-12T17:19:09Z",
"sender": "XMD Tag 1",
"message": "@[MapperBot] 37.76985, -122.40525 [0.3w]",
"channel": "#wardriving",
"raw_hex": "150206...",
"path": ["A3", "B0"],
"observers": [
{"name": "Observer1", "snr": 9.5, "rssi": -56, "timestamp": "2026-04-12T17:19:10Z"}
]
}
]
HTML output
Self-contained interactive viewer — search, sortable columns, expandable detail rows:
corescope-decrypt --channel "#wardriving" --db meshcore.db --format html --output wardriving.html
open wardriving.html
No external dependencies. The JSON data is embedded directly in the HTML file.
IRC / log output
Plain-text, one line per message — ideal for grep, awk, and piping:
corescope-decrypt --channel "#wardriving" --db meshcore.db --format irc
[2026-04-12 17:19:09] <XMD Tag 1> @[MapperBot] 37.76985, -122.40525 [0.3w]
[2026-04-12 17:20:25] <XMD Tag 1> @[MapperBot] 37.78075, -122.39774 [0.3w]
[2026-04-12 17:25:30] <mk 🤠> @[MapperBot] 35.32444, -120.62077
# Find all messages from a specific sender
corescope-decrypt --channel "#wardriving" --db meshcore.db --format irc | grep "KE6QR"
How channel encryption works
MeshCore hashtag channels derive their encryption key from the channel name:
- Key derivation:
AES-128 key = SHA-256("#channelname")[:16](first 16 bytes) - Channel hash:
SHA-256(key)[0]— 1-byte identifier in the packet header, used for fast filtering - Encryption: AES-128-ECB
- MAC: HMAC-SHA256 with a 32-byte secret (key + 16 zero bytes), truncated to 2 bytes
- Plaintext format:
timestamp(4 LE) + flags(1) + "sender: message\0"
See the firmware source at firmware/src/helpers/BaseChatMesh.cpp for the canonical implementation.
Testing against the fixture DB
cd cmd/decrypt
go test ./...
# Manual test with the real fixture:
go run . --channel "#wardriving" --db ../../test-fixtures/e2e-fixture.db --format irc
The shared crypto library also has independent tests:
cd internal/channel
go test -v ./...
Limitations
- Hashtag channels only. Only channels where the key is derived from
SHA-256("#name")are supported. Custom PSK channels require the raw key (not implemented). - No DM decryption. Direct messages (
TXT_MSG) use per-peer asymmetric encryption and cannot be decrypted by this tool. - Read-only. The tool opens the database in read-only mode and never modifies it.
- Timestamps are UTC. The sender's embedded timestamp is used when available, displayed in UTC.