Phasmid is a field-evaluation prototype for local-only coercion-aware storage. It stores encrypted payloads in vault.bin and requires a password plus a camera-recognized physical object cue before recovery.
The project is intended for USB gadget mode or localhost access. It is not a replacement for full-disk encryption, hardware-backed key storage, audited classified-data handling, or a complete solution to compelled disclosure.
- Initialize an encrypted container.
- Store protected entries in an internal two-slot container model.
- Register and verify camera-based physical object cues.
- Encrypt and retrieve payloads.
- Support normal access and restricted recovery behavior.
- Clear local state through restricted owner-controlled actions.
- Operate from a CLI or local WebUI v2.
- Optionally write a minimal audit log.
| Path | Purpose |
|---|---|
main.py |
Local CLI launcher |
src/phasmid/cli.py |
CLI implementation |
src/phasmid/vault_core.py |
Encrypted container logic |
src/phasmid/ai_gate.py |
Camera input and object-cue orchestration |
src/phasmid/camera_frame_source.py |
OpenCV camera capture lifecycle |
src/phasmid/object_cue_matcher.py |
ORB-based object-cue matching logic |
src/phasmid/object_cue_store.py |
Encrypted object-cue reference persistence |
src/phasmid/face_sample_matcher.py |
Face sample extraction and template comparison |
src/phasmid/local_state_crypto.py |
Shared AES-GCM helper for local state blobs and templates |
src/phasmid/web_server.py |
FastAPI Web UI/API |
src/phasmid/bridge_ui.py |
OpenCV status UI |
src/phasmid/emergency_daemon.py |
Panic trigger watcher and local access-path clear flow |
src/phasmid/audit.py |
Optional audit log |
src/phasmid/config.py |
Shared state names and runtime policy |
src/phasmid/templates/ |
WebUI v2 server-rendered templates |
scripts/bench_kdf.py |
Argon2id benchmark helper |
docs/THREAT_MODEL.md |
Threat model |
tests/ |
Unit tests |
| Path | Purpose |
|---|---|
vault.bin |
Encrypted container |
.state/store.bin |
Encrypted object-cue state blob |
.state/lock.bin |
Local key for object-cue state encryption |
.state/access.bin |
Local access key required to recover vault.bin |
.state/signal.key |
Panic trigger token |
.state/signal.trigger |
Panic trigger file |
.state/events.log |
Optional audit log |
.state/events.auth |
Optional audit verifier material |
.state/face.enroll |
Short-lived first-time face enrollment request |
The default state directory is .state/ and can be changed with PHASMID_STATE_DIR. The directory is intended to be mode 0700; sensitive files are intended to be mode 0600. Neutral filenames reduce obvious metadata, but they do not provide deniability.
New local state code paths should use the typed state store for schema-versioned records, atomic writes, restrictive file permissions, and explicit transition checks. Existing binary state files remain managed by their owning modules until a migration path is defined.
Phasmid defines a local cryptographic primitive boundary in src/phasmid/crypto_boundary.py. Startup checks cover AES-GCM round trip behavior, HMAC-SHA-256 behavior, and random byte generation health. Failure causes local startup to stop with a neutral message in the CLI path.
This boundary improves reviewability and failure detection. It is not a FIPS validation, certification claim, or replacement for independent cryptographic review.
The container uses two fixed internal storage spans. The CLI keeps a compact --entry a / --entry b selector, while WebUI v2 maps the internal model to neutral protected-entry workflows and does not expose internal labels during normal operation.
phasmid initThis rotates the local access key, overwrites vault.bin with random data, and leaves an empty container ready for new entries.
phasmid store --entry a --file path/to/file
phasmid store --entry b --file path/to/fileStore flow:
- Start the camera gate.
- Prompt for normal access and restricted recovery passwords.
- Reject empty, duplicate, short, or highly repetitive passphrases.
- Register the physical object cue for the selected internal entry.
- Read the input file.
- Derive a key with Argon2id.
- Encrypt the payload with AES-GCM.
phasmid retrieve --out output.binRetrieve flow:
- Start the camera gate.
- Prompt for the vault password.
- Verify the registered physical object cue.
- Attempt recovery against internal candidates.
- Write or display the recovered payload if access succeeds.
- Apply restricted recovery behavior only when the password or configured policy requires it.
These settings and passwords can cause data loss:
PHASMID_PURGE_CONFIRMATION=0PHASMID_DURESS_MODE=1- restricted recovery passwords
phasmid brickThis flow destroys .state/access.bin first, then performs a best-effort overwrite of vault.bin. Flash media, snapshots, backups, and journaling filesystems may retain old data. Recovery resistance depends primarily on destruction, rotation, or removal of required key material, not on overwrite guarantees.
The WebUI is managed directly from the TUI Operator Console (hotkey w).
The TUI provides a high-visibility warning banner when the WebUI is active and includes an Auto-Kill Timer. If the TUI detects no operator input for 10 minutes while the WebUI is running, it will automatically terminate the WebUI subprocess to return the system to a stealth state.
To start the server manually:
PYTHONPATH=src python3 -m phasmid.web_serverThe default bind address is 127.0.0.1:8000.
WebUI v2 is server-rendered with lightweight JavaScript. It preserves the internal two-slot model while presenting normal operations as protected-entry workflows.
Normal navigation:
- Home
- Store
- Retrieve
- Maintenance
The restricted action view is available only by direct route and is not shown in normal navigation. A direct GET /emergency renders only a restricted confirmation screen until the browser has a fresh restricted confirmation session. After confirmation, the page presents a short stepwise emergency flow with exact-phrase prompts and a visible restricted-confirmation lifetime. Hidden route concealment is not a security boundary.
PHASMID_FIELD_MODE=1 reduces normal Maintenance detail for appliance use. Before restricted confirmation, Maintenance shows only general health, local-only posture, and a confirmation requirement for sensitive maintenance. It hides state paths, audit export, token rotation, and detailed diagnostics until a fresh restricted confirmation is active. The hidden restricted action route also uses a quieter stepwise emergency flow intended to reduce operator confusion during high-stress local actions.
Field Mode is not a security boundary. It reduces casual local exposure in the WebUI and maintenance APIs. It does not prevent forensic inspection, filesystem analysis, memory capture, host compromise, browser compromise, physical coercion, or lawful compulsory process.
Hidden restricted routes are UX concealment only. They are not access control by themselves. High-risk actions are evaluated through a shared local policy layer that combines deployment-mode capability, local tokens, restricted confirmation freshness, and typed confirmation where applicable.
WebUI responses include conservative browser hardening headers such as no-store cache control, frame denial, MIME-sniffing protection, a local-only content security policy, no-referrer policy, and limited browser permissions. These headers reduce browser-visible residue and common embedding or caching risks. They do not make the WebUI suitable for untrusted networks and are not a substitute for local-only binding, host integrity, or operator discipline.
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Home |
GET |
/store |
Store screen |
GET |
/retrieve |
Retrieve screen |
GET |
/maintenance |
Maintenance screen |
GET |
/maintenance/entries |
Entry management screen |
GET |
/emergency |
Hidden restricted action screen |
GET |
/video_feed |
Camera stream for the active local WebUI session |
GET |
/status |
Neutral device/object status |
POST |
/restricted/confirm |
Short-lived restricted confirmation |
POST |
/register_key |
Bind or rebind a physical object |
POST |
/store |
Store a protected entry |
POST |
/metadata/check |
Local metadata risk check |
POST |
/metadata/scrub |
Best-effort local metadata reduction |
POST |
/retrieve |
Retrieve and download the matching entry |
POST |
/purge_other |
Hidden restricted clear action |
POST |
/emergency/initialize |
Hidden container initialization |
POST |
/emergency/brick |
Hidden local access-path clear action |
GET |
/maintenance/diagnostics |
Local diagnostics |
POST |
/maintenance/rotate_token |
Rotate Web mutation token |
POST |
/maintenance/reset_session |
Reset local session counters |
GET |
/maintenance/logs |
Export optional local audit log |
Mutating endpoints require X-Phasmid-Token. Sensitive endpoints also require a short-lived restricted confirmation session and typed action confirmation where applicable. Entry Management withholds binding details until restricted confirmation is active and returns only selected-entry neutral status.
/status intentionally returns only neutral fields:
camera_readyobject_statedevice_statelocal_mode
The normal UI must not display internal entry labels, internal retrieval order, or restricted local-state behavior after retrieval.
Detailed maintenance diagnostics may include neutral hardware-binding availability fields after restricted confirmation or when Field Mode is not suppressing detail.
WebUI exposure is bounded by explicit operator start from the TUI, local-only binding to 127.0.0.1 by default, and TUI-managed auto-kill on inactivity.
No capture-visible surface should reveal the internal disclosure model, internal trial order, slot purpose, restricted recovery side effects, or the existence of an alternate protected state.
Capture-visible surfaces include the WebUI, rendered HTML, browser history, browser cache, JavaScript console, response headers, download filenames, CLI output, shell history, systemd stdout/stderr, audit logs, state-directory filenames, screenshots, and documentation copied to the device.
Common user-facing wording should be centralized where practical. New UI, CLI, API, and audit text must either reuse shared neutral strings or pass terminology audit before release.
Phasmid must prefer simple, low-choice flows under stress.
Normal operation should remain:
- Store
- Retrieve
- Maintenance
Restricted actions must remain separated. Field Mode should reduce diagnostic noise. The UI should avoid forcing the user to reason about internal slots, trial order, recovery side effects, or disclosure structure during stressful conditions.
Store provides a local-only metadata risk check. It does not call cloud services and does not send telemetry.
/metadata/check and /metadata/scrub enforce the normal Web mutation token, rate limiting, and upload size limit. Uploaded data is processed in memory; these routes do not require writing the uploaded file to disk, and the intended implementation property is no disk write for uploaded metadata inspection.
The initial checker warns about common risks:
- GPS-like image metadata;
- camera maker or model metadata;
- device serial-like fields;
- document author fields;
- creator application fields;
- document title or subject fields;
- embedded thumbnails;
- local path leakage;
- original filename context.
Storage is not blocked by default. The UI offers continue, best-effort metadata reduction when supported, or cancel.
Best-effort metadata reduction is conservative. It never overwrites the original file unless a future explicit option is added. Unsupported file types fail safely. Downloads use a neutral filename and must not expose the original filename in response headers. Metadata removal is best-effort and may not remove every embedded identifier from every file format.
The Store screen includes a short reminder: store only what is needed and separate identities, evidence, notes, and operational context when possible.
The current format is JES v3.
- No plaintext magic/header.
- Fixed-width internal storage spans.
- Each span contains a normal access slot and a restricted recovery slot.
- Per-record random salt and nonce.
- AES-GCM authenticated encryption.
- Filename and payload metadata are encrypted.
- v1/v2 compatibility retrieval has been removed.
Argon2id inputs:
- User password
- Physical-object cue token
- Internal mode
- Password role
- Per-record random salt
.state/access.bin- Optional external values from
PHASMID_HARDWARE_SECRET_FILE,PHASMID_HARDWARE_SECRET, orPHASMID_HARDWARE_SECRET_PROMPT
Default Argon2id parameters are tuned for Raspberry Pi Zero 2 W class hardware: memory_cost=32768, iterations=2, lanes=1.
Recommended field hierarchy:
- strong access password;
- physical-object cue;
.state/access.bin;- optional external value via
PHASMID_HARDWARE_SECRET_FILEorPHASMID_HARDWARE_SECRET_PROMPT=1.
For high-risk deployments, do not store all recovery conditions on the same physical medium. Phasmid is strongest when the encrypted container, local state, memorized password, physical-object cue, and optional external key material are separated.
Phasmid extracts ORB features from camera frames.
Registration:
- Capture several frames over a short interval.
- Select the candidate with the most keypoints.
- Reject low-feature images.
- Reject candidates too similar to an existing object cue.
- Store templates together in encrypted
.state/store.bin.
Retrieval:
- Extract ORB features from current frames.
- Match against encrypted reference templates.
- Require enough good matches and homography inliers.
- Require stable matching in at least 3 of the last 5 frames.
- Reject ambiguous matches.
The physical object is an operational cue, not a high-entropy cryptographic factor.
Multi-object and visual-sequence cue extensions are analysis-only at this stage. Any future implementation must preserve neutral capture-visible behavior, explicit ambiguity rejection, and no direct cryptographic dependence on unstable image coordinates.
An experimental policy-layer prototype can evaluate neutral frame signals (for example none|detected|matched|ambiguous) and optional short token sequences, but this remains a local operational gate decision and not a cryptographic input path.
| Variable | Purpose | Default |
|---|---|---|
PHASMID_STATE_DIR |
Runtime state directory | .state |
PHASMID_STATE_SECRET |
External value for object-cue state encryption | unset |
PHASMID_HARDWARE_SECRET_FILE |
External value file mixed into Argon2id | unset |
PHASMID_HARDWARE_SECRET |
External value string mixed into Argon2id | unset |
PHASMID_HARDWARE_SECRET_PROMPT |
Prompt for an external value | unset |
PHASMID_PURGE_CONFIRMATION |
Require explicit confirmation for configured recovery behavior | 1 |
PHASMID_DURESS_MODE |
Enable opt-in access-triggered local-state update | 0 |
PHASMID_WEB_TOKEN |
Web mutation token | random at start |
PHASMID_HOST |
Web bind host | 127.0.0.1 |
PHASMID_PORT |
Web bind port | 8000 |
PHASMID_MAX_UPLOAD_BYTES |
Web upload limit | 26214400 |
PHASMID_RESTRICTED_SESSION_SECONDS |
Restricted confirmation lifetime | 120 |
PHASMID_FIELD_MODE |
Reduce normal WebUI operational detail | 0 |
PHASMID_PROFILE |
Select local capability mode: standard, field, or maintenance |
standard |
PHASMID_MIN_PASSPHRASE_LENGTH |
Minimum Store passphrase length | 10 |
PHASMID_ACCESS_MAX_FAILURES |
Failed access attempts before temporary lockout | 5 |
PHASMID_ACCESS_LOCKOUT_SECONDS |
Temporary access lockout duration | 60 |
PHASMID_AUDIT |
Enable audit logging | 0 |
PHASMID_AUDIT_FILENAMES |
Record filename hashes | unset |
Phasmid documentation defines neutral mission presets rather than role-revealing UI labels. Examples include:
- Local Notes;
- Temporary Holding;
- Protected Material;
- Travel Set;
- Review Set;
- Research Material.
Presets should influence guidance such as metadata warning, external key material recommendation, retention reminders, audit disabled by default, Field Mode recommendation, and entry separation guidance.
Retention principle: the safest sensitive data is data not carried. Users should remove stale entries after the task or trip, avoid old contact lists, avoid mixing unrelated work in one local entry, and review contents before checkpoints or inspection events.
On flash media, complete overwrite-based deletion cannot be guaranteed across every storage layer. Phasmid therefore treats restricted recovery primarily as key-path invalidation and key-material destruction, with best-effort overwrite as a secondary measure.
Restricted recovery must not be represented as guaranteed secure deletion. User-facing surfaces should use neutral terms such as restricted local update, local access path, key material, and best-effort overwrite.
The following table defines the mandatory ordering for local access-path clear operations. Key-material destruction must precede any container overwrite. This ordering ensures that the container ciphertext becomes unrecoverable even if the overwrite fails or is partially reversed by the storage layer.
| Step | Action | Target | Effect |
|---|---|---|---|
| 1 | Overwrite + remove | .state/access.bin |
Local access key destroyed; Argon2id derivation path broken |
| 2 | Best-effort overwrite | vault.bin (full or partial slot) |
Container ciphertext randomized; recovery requires the key from step 1 |
Step 1 alone makes recovery infeasible: the Argon2id key cannot be recomputed without the local access key, even if the attacker has a copy of vault.bin and the user's passphrase. Step 2 is a secondary, best-effort measure.
Tests confirming this ordering and behavior are in tests/test_vault_core.py under the "Key-material invalidation sequence" section.
This invalidation sequence applies to: vault.silent_brick(), vault.purge_mode(), and the restricted-recovery path triggered by the restricted recovery password (PURGE_ROLE).
The v3 container format uses inline string concatenation for domain separation in the Argon2id context string. The v4 design introduces a second derivation stage via HKDF-SHA-256 that produces cryptographically independent, explicitly labelled subkeys from a single Argon2id output.
Argon2id(passphrase + local_key + hardware_secret, salt)
→ 32-byte IKM
→ HKDF-SHA-256(IKM, info=<label>) → vault open subkey
→ HKDF-SHA-256(IKM, info=<label>) → vault purge subkey
→ HKDF-SHA-256(IKM, info=<label>) → local state subkey
→ HKDF-SHA-256(IKM, info=<label>) → audit HMAC subkey
| Label | Purpose |
|---|---|
phasmid-v4:vault:open:1 |
AES-GCM key for the OPEN recovery slot |
phasmid-v4:vault:purge:1 |
AES-GCM key for the PURGE recovery slot |
phasmid-v4:state:1 |
AES-GCM key for local state blobs |
phasmid-v4:audit-hmac:1 |
HMAC-SHA-256 key for audit record chaining |
Label format: phasmid-v4:<purpose>:<version>. The version suffix is incremented when the purpose changes semantically. This decouples label evolution from container format changes.
Argon2id provides memory-hard password stretching and is not replaced. HKDF-SHA-256 is used exclusively for domain-separated subkey derivation from the Argon2id output. HKDF does not replace or weaken the Argon2id stage.
v3 containers remain valid under the v3 KDFEngine path. A v4 container uses src/phasmid/kdf_subkeys.py for the second derivation stage. Migration: retrieve with v3, re-store with v4.
This design is not a FIPS validation claim and does not imply certification. It is an engineering improvement to domain separation within the local key derivation pipeline.
Test vectors and domain separation tests are in tests/test_kdf_subkeys.py.
Raspberry Pi Zero 2 W appliance assumptions are documented in docs/RPI_ZERO_APPLIANCE_DEPLOYMENT.md. The recommended appliance posture is local-only binding, USB gadget access, SSH disabled after provisioning, Wi-Fi and Bluetooth disabled unless explicitly needed, dedicated service user, systemd hardening, Field Mode, audit disabled by default, debug disabled by default, no telemetry, no cloud dependency, no remote management, and external key-material separation.
Seizure review requirements are documented in docs/SEIZURE_REVIEW_CHECKLIST.md. Review normal screens, restricted pages before and after confirmation, browser history, cache, HTML source, JavaScript console, HTTP response headers, download filenames, optional audit logs, .state/ names, temporary files, shell history, systemd logs, CLI output, environment variables, and service unit files.
Operational guidance documents include docs/SOURCE_SAFE_WORKFLOW.md, docs/SEIZURE_REVIEW_CHECKLIST.md, docs/FIELD_TEST_PROCEDURE.md, docs/REVIEW_VALIDATION_RECORD.md, docs/OPERATIONS.md, docs/RESTRICTED_ACTIONS.md, and docs/STATE_RECOVERY.md.
Optional audit records are versioned and include sequence, previous record hash, record hash, and local verifier fields. This improves local tamper review when audit logging is enabled, but it also creates additional local metadata and is disabled by default.
python3 -m unittest discover -s testsKDF benchmark:
python3 scripts/bench_kdf.pyThis build reads and writes JES v3 records only. Earlier development containers that depend on superseded internal record labels are not supported and should be reinitialized before use.
Phasmid does not guarantee protection against a compromised OS, live memory capture, keylogging, camera observation, forced disclosure, complete secure deletion, deniability, or unsafe network exposure.
Phasmid does not claim software existence concealment. Discovery of project files, binaries, logs, or deployment traces can reveal that coercion-aware storage software is present.
The intended claim boundary is:
- controlled disclosure is in scope;
- data-existence deniability is partial and scenario-dependent;
- software existence concealment is out of scope.