DEXBot2 implements a layered security model to protect private keys and credentials at rest, in transit, and in RAM during a live session.
The credential daemon employs a strictly enforced HMAC policy engine. All operations require cryptographic validation. The daemon dynamically loads parameters, ensuring that bots cannot bypass verification or hit unauthorized resource limits.
The daemon supports persistent operations via session IDs. To mitigate
interruptions (e.g., daemon restarts or TTL expiration), the system implements
a transparent renegotiation loop. When an executeViaDaemonToken call fails,
the system automatically fetches a new sessionId, injects it into the
signingToken, and cleanly replays the pending operations.
The credential daemon enforces granular operation policies via daemon-policies.json.
These policies are strictly enforced at the daemon boundary. To prevent
resource exhaustion, the daemon enforces a global maxOpsPerBatch limit
(defaulting to 200). This ensures that complex grid replacements or
batch orders do not overwhelm the daemon's internal state.
To minimize the window of exposure for sensitive key material, the credential
daemon implements explicit memory scrubbing on a best-effort basis. All sensitive buffers—
including vault secrets, session secrets, and cached account keys—are
explicitly overwritten with zeros (via Buffer.fill(0)) upon process
termination, ensuring no plaintext material persists in RAM after the
daemon exits.
DEXBot2 keeps private keys out of the bot process entirely. A dedicated
credential daemon holds the vault secret in memory and serves signing
requests over a local socket. The primary bot flow uses a signing token to
tell the daemon which account to use; the daemon signs or broadcasts operations
internally and returns results. A private-key compatibility path still exists
for legacy clients, so the stronger guarantee is that the raw key does not leave
the daemon process unless that compatibility path is used.
The full chain from user password to on-chain operation looks like this:
Master password
│
▼ scrypt (N=2¹⁷, r=8, p=1)
Vault key ──────────────────────────────┬── HMAC-SHA256 (vault verifier)
│ │
▼ HKDF-SHA256 (per-record salt) ▼
Record key → AES-256-GCM → keys.json timingSafeEqual on unlock
│
▼ daemon startup
Session secret (HKDF-SHA256, new random salt each run)
│
▼ AES-256-GCM re-encrypt
In-RAM session cache (encrypted entries)
│
▼ signing token handed to bot
Daemon signs/broadcasts → result returned to bot
The master password is never stored. It is run through scrypt to produce a 32-byte vault key:
| Parameter | Value | Purpose |
|---|---|---|
| N | 2¹⁷ (131 072) | Memory-hard work factor |
| r | 8 | Block size |
| p | 1 | Parallelism |
| dkLen | 32 bytes | AES-256 key length |
| salt | 16 random bytes, stored in vault | Prevents rainbow tables |
| maxmem | 256 MB | Caps memory usage |
This makes offline brute-force attacks against a stolen keys.json expensive.
The vault key is never used directly to encrypt a private key. Each record gets its own 16-byte random salt, and a record-specific key is derived via HKDF-SHA256:
record key = HKDF-SHA256(
ikm = vault key,
salt = random 16 bytes (stored with the record),
info = "dexbot2:v2:record-key"
)
This means compromising one record key does not help an attacker decrypt any other record.
Each private key is encrypted with AES-256-GCM:
- 12-byte random IV per encryption operation
- 16-byte GCM authentication tag (detects tampering)
- Stored format:
v2:<recordSalt>:<iv>:<authTag>:<ciphertext>(all hex)
A short HMAC-SHA256 of a fixed label under the vault key is stored in
keys.json. On unlock, the candidate vault key is reproduced and its HMAC is
compared with crypto.timingSafeEqual to prevent timing attacks. If the
comparison fails, the wrong password was supplied — no key material is ever
decrypted.
The daemon (credential-daemon.js) is a long-running local process that holds
the vault key and session cache in RAM. Callers communicate with it over a Unix
domain socket; the main signing flow never hands raw key bytes to the bot.
- Launcher creates a one-shot bootstrap socket in a freshly created
mkdtempdirectory (chmod 0700) and passes its path to the daemon child process via theDEXBOT_CRED_BOOTSTRAP_SOCKETenvironment variable. - The daemon reads the variable, immediately deletes it from
process.envto reduce exposure to child processes and later inspection, then connects back and requests the secret. - Once the secret is transferred the bootstrap server closes, the socket and temp directory are removed, and the bootstrap path becomes unreachable.
- Daemon loads
keys.jsoninto memory, builds the session cache (see §3). - Daemon writes a ready file and begins accepting signing requests on the main socket. The bootstrap socket no longer exists at this point.
A configurable timeout (default: a few seconds) aborts the entire bootstrap if the daemon does not connect in time, preventing the bootstrap socket from being left open indefinitely.
| Request type | What it does |
|---|---|
probe-account |
Confirms an account is available (no key material returned) |
broadcast-operation |
Signs and broadcasts a single operation; returns result |
execute-operations |
Signs and broadcasts a batch; returns result |
private-key |
Legacy compatibility path for older clients |
The primary bot flow only calls probe-account, broadcast-operation, or
execute-operations. Older clients can still request private-key directly
when they need a compatibility path.
The bot receives a signing token at startup:
{
kind: 'dexbot-daemon-signing-token',
accountName: '<account>',
socketPath: '/run/user/<uid>/dexbot2/dexbot-cred-daemon.sock'
}This token carries no key material. If intercepted, an attacker can only submit signing requests to the daemon for the named account while the daemon is running — they cannot extract the private key.
When the daemon starts, every account key is re-encrypted under a session secret that is freshly randomized each run. This is the "temporary key" mechanism:
session secret = HKDF-SHA256(
ikm = vault key,
salt = random 16 bytes (generated at daemon start, never persisted),
info = "dexbot2:v2:session-key"
)
All private keys are then re-encrypted with AES-256-GCM under this session
secret and stored in a Map in RAM. Consequences:
- No plaintext keys are retained in the cache. Decrypted keys exist only transiently while a request is being serviced, then are re-encrypted under the session secret.
- Session isolation. A memory snapshot from one run cannot be replayed into another because the session salt is never written to disk.
- Vault fallback. If
keys.jsonis readable, the daemon always re-derives the key from disk on a cache miss, keeping the session cache fresh after key rotation or new account additions — without a restart.
The daemon communicates over a Unix domain socket. All runtime paths are validated before use or before any stale path is removed.
The runtime directory defaults to $XDG_RUNTIME_DIR/dexbot2/ when
$XDG_RUNTIME_DIR is usable; otherwise it falls back to profiles/run/ under
the repository root. In both cases it is created with mode 0700 (owner
read/write/execute only) and verified at every startup.
Both the socket (dexbot-cred-daemon.sock) and the ready file
(dexbot-cred-daemon.ready) are chmod'd to 0600 after creation.
Before trusting or unlinking either path, the code asserts all of the following:
| Check | Requirement |
|---|---|
| Symbolic link | Refused — lstat is used, not stat |
| File type | Must match expected type (socket or file) |
| Owner UID | Must match the current process UID |
| Permissions | Must be exactly 0600 |
A stale socket that fails any of these checks is not removed, preventing a malicious process from placing a rogue socket at the expected path and having the daemon silently unlink it and take over.
Interactive master-password attempts are capped at 3. Once the budget is exhausted:
- An unambiguous PM2-compatible error message is printed.
- The process exits immediately.
- No partial state is left behind.
The failure path is consistent regardless of whether the daemon or the interactive password prompt handled the authentication, making the output predictable for monitoring and alerting.
Older vaults stored a plain SHA-256 hash of the master password for verification. This hash is deliberately weak by modern standards. During the first successful unlock of a legacy vault, the code:
- Verifies the password against the SHA-256 hash.
- Re-encrypts all keys under the v2 scrypt-derived vault key.
- Writes the HMAC-SHA256 vault verifier.
- Deletes
masterPasswordHashfrom the vault file.
After migration the weak hash is gone and subsequent unlocks use only the HMAC-SHA256 verifier. Legacy data cannot be decrypted without first migrating through this path.
bot.js / dexbot.js
│
▼ probe daemon (probe-account)
Daemon healthy?
├── YES → obtain signing token → start bot with token
└── NO → fall back to interactive master-password prompt
│
▼ attempts exhausted?
YES → print failure message, exit
This means production deployments running the daemon never expose the master password interactively, while the interactive path remains available for development and recovery.
| Technique | Where applied | Purpose |
|---|---|---|
| scrypt (N=2¹⁷) | Password → vault key | Memory-hard KDF; resists brute force |
| HKDF-SHA256 (per-record) | Vault key → record key | Key isolation per account |
| HKDF-SHA256 (random salt) | Vault key → session key | Ephemeral RAM-only re-encryption |
| AES-256-GCM | All encryption operations | Authenticated encryption; detects tampering |
| HMAC-SHA256 | Vault verifier | Unlock check without storing the password |
crypto.timingSafeEqual |
Verifier comparison | Prevents timing-based password oracle |
| Batch limit (200) | execute-operations |
Prevents resource exhaustion |
| Signing token (no key export) | Bot ↔ daemon IPC | Private key never leaves daemon boundary |
lstat + owner/mode/type checks |
Runtime socket & ready file | Prevents symlink attacks and rogue sockets |
| 0700 runtime dir / 0600 sockets | Filesystem | OS-level access restriction |
| Random session salt (not persisted) | Session cache | Memory snapshot from one run is useless in another |
| One-shot bootstrap socket (mkdtemp 0700, auto-cleanup) | Secret handoff to daemon | Secret is never written to disk; socket destroyed after first use |
delete process.env.DEXBOT_CRED_BOOTSTRAP_SOCKET |
Daemon startup | Socket path cannot be inherited by child processes or read from /proc |
| Attempt limit (3) + immediate exit | Interactive auth | Limits online brute-force window |
| SHA-256 hash deleted after migration | Legacy vault upgrade | Weak verifier removed on first successful unlock |