Files
meshcore-analyzer/cmd/decrypt
Kpa-clawbot c233c14156 feat: CLI tool to decrypt and export hashtag channel messages (#724)
## 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>
2026-04-12 22:07:41 -07:00
..

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:

  1. Key derivation: AES-128 key = SHA-256("#channelname")[:16] (first 16 bytes)
  2. Channel hash: SHA-256(key)[0] — 1-byte identifier in the packet header, used for fast filtering
  3. Encryption: AES-128-ECB
  4. MAC: HMAC-SHA256 with a 32-byte secret (key + 16 zero bytes), truncated to 2 bytes
  5. 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.