~ober/jerboa-signal

Imported from ~/mine/jerboa-signal

download snapshot

about

# jerboa-signal

A Signal client written in [Jerboa](https://git.jerboa.sh/ober/jerboa), driving
[signal-cli](https://github.com/AsamK/signal-cli) (Java) as a JSON-RPC
subprocess. All orchestration, arg parsing, and message routing is Jerboa;
the Signal protocol itself is delegated to signal-cli's `libsignal-service-java`.

## What it does

Bidirectional bridge between Signal and your shell:

```
jerboa-signal send [-a +PHONE] RECIPIENT MESSAGE   # send one message, exit
jerboa-signal listen [-a +PHONE]                   # stream inbound as NDJSON
jerboa-signal tui [-a +PHONE]                      # terminal UI shell
```

`-a` is required only if multiple Signal accounts are linked to signal-cli.

## Prerequisites

- `signal-cli` 0.14.5 or newer on `PATH` (Homebrew may lag; the local
  `scripts/signal-cli-jvm` wrapper uses `~/.local/opt/signal-cli-0.14.5`).
  Older 0.14.4.1 builds drop current sealed-sender receive envelopes with
  `getServerGuid(...) must not be null`.
  Release evidence currently reviews exactly `signal-cli 0.14.5`; a newer
  installed version must be reviewed before publication.
- Jerboa build tools. `make binary`/`make install` use project-local
  `./jerbuild` first, then `.jerboa/bin`, then `PATH`, and download the
  matching release artifact if none are available.

## One-time setup: link as a secondary device

jerboa-signal expects signal-cli to already be linked. Run this once:

```sh
signal-cli link -n "jerboa-signal"
```

It prints a `sgnl://linkdevice?uuid=...` URI. On your phone:
**Signal → Settings → Linked devices → "+" → scan/paste the URI**. signal-cli
will register the device and exit. Verify:

```sh
signal-cli listAccounts
```

## Usage

```sh
make install                                   # installs ~/.local/bin/jerboa-signal
jerboa-signal send +15551234567 "hello world"
jerboa-signal listen | jq .                    # pretty-print incoming envelopes
jerboa-signal --trace /tmp/jerboa-signal.trace tui
```

Without `make install`, run via the Makefile:

```sh
make run ARGS="send +15551234567 hello"
make run ARGS="listen"
make run-tui
```

The TUI needs the small termbox2 shim:

```sh
make tui-shim
./jerboa-signal tui
```

The TUI opens `signal-cli jsonRpc --receive-mode manual`, then subscribes with
`subscribeReceive` once the app reader is active, so new Signal events arrive
while it is running without racing the UI startup. At startup it preloads
active groups and known contacts into the conversation pane. The current interface keeps
in-memory conversations for this session, routes incoming
messages, edits, reactions, deletes, calls, receipts, and typing events into
threads, and sends plain text from the
composer to the selected conversation with Enter. Use Up/Down to change the
selected conversation. Use Ctrl-N to open the new-message picker, type to
filter contacts/groups or enter a `+number`, and press Enter to select. Use
Ctrl-F to search message history across all conversations at once: type a
query, use Up/Down to move through matches, and press Enter to jump to the
conversation holding that message. Search matches message text and sender, and
covers everything received or sent during the current session — history is
in-memory only, so it does not include messages from before the TUI was
opened. Use
Ctrl-D to delete the selected conversation; a y/n prompt confirms first. For a
direct contact this calls signal-cli `removeContact --forget`, which erases all
local Signal data for that contact (identity keys, sessions, profile) — use it
to remove someone entirely. For a group it just drops the conversation from the
list. Deleted conversations are remembered in
`~/.local/share/jerboa-signal/removed-<account>.txt` and stay hidden across
relaunches, even though signal-cli (a linked device) keeps re-syncing your
phone's contacts. If a removed person messages you again the thread reappears;
to remove someone from your Signal account everywhere, delete them on your phone
too. Use Ctrl-Q or Ctrl-C to quit.

## Debug tracing

Runtime tracing is opt-in and writes a chmod 600 file:

```sh
jerboa-signal --trace /tmp/jerboa-signal.trace tui
```

The default trace records safe internal telemetry such as send phases, timings,
byte counts, log backend selection, history replay counts, and encrypted log
persistence stages. It does not include message bodies or raw Signal RPC frames.

For maximum debugging detail, including raw RPC frames and signal-cli verbose
stderr, use:

```sh
jerboa-signal --trace-all /tmp/jerboa-signal.raw.trace tui
```

That file is sensitive: it can include message content and Signal identifiers.
To force the encrypted log to be rewritten after every row while debugging
durability issues, use:

```sh
jerboa-signal --trace-all /tmp/jerboa-signal.raw.trace --log-persist immediate tui
```

The default is `batch`: the encrypted container is rewritten after 64 rows,
1 MiB of new retained payload, or two seconds, whichever comes first, and at
clean close. This avoids whole-database work for every message while bounding
the crash window. `--log-persist immediate` forces a rewrite after every row;
`--log-persist close` persists only at clean exit. Operators can lower the
bounded batch thresholds with `JERBOA_SIGNAL_LOG_BATCH_ROWS`,
`JERBOA_SIGNAL_LOG_BATCH_BYTES`, and `JERBOA_SIGNAL_LOG_BATCH_MS`.

## Security

`jerboa-signal` is not a Signal protocol implementation. It delegates Signal
cryptography, account state, server communication, and device linking to
`signal-cli`. The local security boundary is the Jerboa orchestration layer:
JSON-RPC framing, trace redaction, attachment export, encrypted local message
logs, terminal UI FFI, and process execution.

Before using this on production accounts, read `SECURITY.md`,
`docs/threat-model.md`, `docs/credential-handling.md`, and
`docs/ffi-boundary.md`. Run `make verify` from a clean checkout and preserve
`make release-evidence` output for release candidates. The release evidence
includes SBOM/provenance, repeated binary reproducibility, a no-secret binary
startup smoke, `signal-cli` advisory/version review, and explicit status fields
for live Signal smokes that require a non-production linked account.

## Encrypted message log

The TUI can log every message to an encrypted SQLite database so you keep a copy
even when the sender later deletes it ("delete for everyone"). Logs use
`jsqlite` for new logs and a Jerboa-native encrypted container around the
database image; contents and metadata are unreadable without your passphrase.
Existing SQLCipher logs are still opened through the legacy shim so old history
continues to replay.

Enable it:

```sh
./jerboa-signal tui           # prompts: passphrase for encrypted message log
```

At startup the TUI asks for a passphrase on the controlling terminal. It fails
closed if the exact terminal state cannot be saved, echo disabled, or the state
restored. Enter one to log; leave it blank to skip logging for that session.
For unattended use, prefer `--log-key-fd FD` with a dedicated inherited FD.
`JERBOA_SIGNAL_DB_KEY` remains available but exposes the secret to the process
environment.

The log lives at `~/.local/share/jerboa-signal/messages-<account>.db`. Each
inbound notification and outbound send is one append-only row (with the full raw
JSON); remote-deletes, edits, reactions, calls, attachments, and other
displayable events are logged as new rows and never overwrite the
original.

Retention is bounded by default to 20,000 rows, 128 MiB of retained payload,
and 4 MiB per entry; oldest rows are pruned before an insert exceeds the row or
payload budget. The limits can be lowered with `JERBOA_SIGNAL_LOG_MAX_ROWS`,
`JERBOA_SIGNAL_LOG_MAX_PAYLOAD_BYTES`, and
`JERBOA_SIGNAL_LOG_MAX_ENTRY_BYTES`.

To convert an older SQLCipher log to the current jsqlite container format:

```sh
jerboa-signal migrate-log     # add -a +PHONE if you use a named account
```

The converter verifies the new row count before swapping files and keeps the
old SQLCipher database beside it as `messages-<account>.db.sqlcipher.bak`.

Logging happens only while the TUI is open — messages that arrive while it is
closed are not captured.

## Architecture

```
┌─────────────────┐  argv      ┌──────────────────┐  NDJSON  ┌────────────┐
│ jerboa-signal   │ ─────────► │ signal/main.ss   │ ───────► │ signal-cli │
│   (CLI argv)    │            │  (subcommands)   │          │  jsonRpc   │
└─────────────────┘            └──────────────────┘ ◄─────── └────────────┘
                                        │           NDJSON          │
                                        ▼                           ▼
                                  stdout / stderr            Signal servers
```

Modules:
- `signal/main.ss` — entry point, argv parsing, subcommand dispatch
- `signal/rpc.ss`  — JSON-RPC client over signal-cli's stdin/stdout (NDJSON framing, id correlation, notification queue)
- `signal/rpc-actor.ss` — long-lived JSON-RPC actor for the TUI
- `signal/cmd-send.ss` — `send` subcommand
- `signal/cmd-listen.ss` — `listen` subcommand
- `signal/cmd-tui.ss` — TUI command entry point; see `docs/TUI_PLAN.md`
- `signal/store.ss` — persistent local state (deleted-conversation list)
- `signal/capture.ss` — normalizes a notification into an encrypted log row
- `signal/logdb.ss` + `signal/log_crypto.ss` — encrypted log backend
- `signal/tui/` — termbox2 FFI and the first terminal shell

## License

Apache-2.0.

recent commits