Skip to content

feat(datastore): add opt-in SQLCipher encryption support#584

Open
TimeToBuildBob wants to merge 5 commits intoActivityWatch:masterfrom
TimeToBuildBob:feat/sqlcipher-encryption
Open

feat(datastore): add opt-in SQLCipher encryption support#584
TimeToBuildBob wants to merge 5 commits intoActivityWatch:masterfrom
TimeToBuildBob:feat/sqlcipher-encryption

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

Summary

Implements encrypted database storage at rest using SQLCipher (via the rusqlite/bundled-sqlcipher feature). Data remains fully encrypted on disk; decryption only happens in-process after the key is supplied.

Closes #435

Design

SQLCipher is a drop-in replacement for SQLite that transparently encrypts every database page. All existing schema/migration code works unchanged — only the connection-open step gains an extra PRAGMA key call.

This is implemented as a Cargo feature flag so the default binary stays unchanged. Users who want encryption build with:

cargo build --no-default-features --features encryption
# or, for a fully self-contained binary that also vendors OpenSSL:
cargo build --no-default-features --features encryption-vendored

Feature flags

Feature SQLite backend OpenSSL
bundled (default) bundled plain SQLite
encryption bundled SQLCipher system OpenSSL required
encryption-vendored bundled SQLCipher vendored via openssl-sys

bundled and encryption* are mutually exclusive (libsqlite3-sys enforces this). Use --no-default-features when enabling encryption.

API changes

aw-datastore:

  • New DatastoreMethod::FileEncrypted(path, key) variant (cfg-gated)
  • New Datastore::new_encrypted(dbpath, key, legacy_import) constructor

aw-server:

  • New --db-password <KEY> CLI flag (also readable from AW_DB_PASSWORD env var)

Usage

# Start server with encryption (key via flag)
aw-server --db-password "my-secret-passphrase"

# Start server with encryption (key via env var — preferred for scripts)
export AW_DB_PASSWORD="my-secret-passphrase"
aw-server

⚠️ Warning: The key is stored in-memory for the lifetime of the process. Passing it on the CLI exposes it in ps output. Prefer AW_DB_PASSWORD in production.

Changes

  • aw-datastore/Cargo.toml — restructure rusqlite features; add encryption and encryption-vendored
  • aw-datastore/src/lib.rs — add DatastoreMethod::FileEncrypted variant
  • aw-datastore/src/worker.rs — open encrypted connection with PRAGMA key; add Datastore::new_encrypted()
  • aw-server/Cargo.toml — forward encryption features from aw-datastore; change aw-datastore dep to default-features = false
  • aw-server/src/main.rs--db-password / AW_DB_PASSWORD option; select new_encrypted() when key is present
  • aw-datastore/tests/datastore.rstest_encrypted_datastore_roundtrip: creates encrypted DB, writes events, closes, reopens with same key, verifies data is intact

Test plan

  • Default build (cargo check) passes with no errors
  • Encryption build: cargo test --no-default-features --features encryption -- test_encrypted_datastore_roundtrip (requires OpenSSL)
  • Manual smoke test: start aw-server --db-password foo, send heartbeats, stop, verify file aw-server-rust.db shows "SQLite database" is no longer readable as plain SQLite

Adds an `encryption` feature flag to aw-datastore (and aw-server) that
enables SQLCipher-based database encryption at rest.

**Usage**:
```
cargo build --no-default-features --features encryption
aw-server --db-password mysecretkey
# or: AW_DB_PASSWORD=mysecretkey aw-server
```

**Changes**:
- aw-datastore: restructure rusqlite features so `bundled` (default) and
  `encryption` (opt-in SQLCipher) are mutually exclusive
- aw-datastore: add `DatastoreMethod::FileEncrypted(path, key)` variant
  applying PRAGMA key after connection open
- aw-datastore: add `Datastore::new_encrypted()` constructor
- aw-server: forward `encryption` / `encryption-vendored` features from
  aw-datastore; accept --db-password / AW_DB_PASSWORD
- tests: add `test_encrypted_datastore_roundtrip` verifying data survives
  a close/reopen cycle with the correct key

Closes ActivityWatch#435
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 27.27273% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.85%. Comparing base (656f3c9) to head (4597c2c).
⚠️ Report is 43 commits behind head on master.

Files with missing lines Patch % Lines
aw-datastore/src/lib.rs 0.00% 4 Missing ⚠️
aw-server/src/main.rs 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #584      +/-   ##
==========================================
- Coverage   70.81%   67.85%   -2.97%     
==========================================
  Files          51       54       +3     
  Lines        2916     3201     +285     
==========================================
+ Hits         2065     2172     +107     
- Misses        851     1029     +178     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 16, 2026

Greptile Summary

This PR adds opt-in SQLCipher encrypted-database support behind encryption / encryption-vendored Cargo feature flags, leaving the default build unchanged. All previously flagged concerns (key exposure via Debug, silent wrong-key panic, AW_DB_PASSWORD silently ignored in non-encryption builds) have been addressed in the current HEAD.

Confidence Score: 5/5

Safe to merge; all previous P0/P1 concerns are resolved and remaining findings are minor test-quality suggestions

The three previously raised blockers (Debug key leakage, silent wrong-key panic, AW_DB_PASSWORD silently ignored) are all fixed in this HEAD. The only remaining findings are P2: test cleanup not guaranteed on assertion failure, no negative test for wrong-key rejection, and the bundled/encryption feature mutual-exclusivity not being Cargo-enforced. None of these affect production correctness.

aw-datastore/tests/datastore.rs — minor test hygiene; aw-datastore/Cargo.toml — feature exclusivity comment

Important Files Changed

Filename Overview
aw-datastore/Cargo.toml Restructures rusqlite features into bundled/encryption/encryption-vendored; mutual exclusivity is documented but not Cargo-enforced
aw-datastore/src/lib.rs Adds cfg-gated FileEncrypted variant with a manual Debug impl that redacts the key — addresses previous P0 concern
aw-datastore/src/worker.rs Opens encrypted connection with PRAGMA key + PRAGMA user_version verification; adds new_encrypted() constructor — previous silent-failure concern addressed
aw-datastore/tests/datastore.rs Adds encrypted roundtrip test; missing guaranteed cleanup on assertion failure and no negative (wrong-key) test case
aw-server/Cargo.toml Correctly forwards encryption feature flags from aw-datastore and sets default-features = false on the dependency
aw-server/src/main.rs Adds db_password CLI/env option; clears env var after reading; panics on AW_DB_PASSWORD set without encryption support compiled in
Cargo.lock Expected lock file update adding openssl-sys dependency for SQLCipher support

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[aw-server starts] --> B{DB_PASSWORD env var set?}
    B -- Yes, encryption compiled --> C[Consume key from CLI/env]
    C --> D[Clear env var for child processes]
    D --> E[Datastore::new_encrypted]
    E --> F[Open SQLCipher connection]
    F --> G[Set encryption cipher via PRAGMA]
    G --> H[Read user_version to verify cipher]
    H -- Failure --> I[panic: bad cipher or not encrypted DB]
    H -- OK --> J[Log: Opened encrypted database]
    J --> K[DatastoreInstance::new - migrations]
    B -- Yes, no encryption compiled --> L[panic: env var set without encryption feature]
    B -- No --> M[Datastore::new - plain SQLite]
    M --> K
    K --> N[Worker thread - request loop]
Loading

Reviews (5): Last reviewed commit: "fix(server): panic when AW_DB_PASSWORD s..." | Re-trigger Greptile

Comment thread aw-datastore/src/worker.rs
Comment thread aw-datastore/src/lib.rs
…ssphrase early

Two security issues flagged by Greptile review:

1. DatastoreMethod derived Debug, which would expose the raw encryption key
   in log output, panic messages, or debug instrumentation. Replace derive
   with a manual Debug impl that redacts the key field as '<redacted>'.

2. PRAGMA key always succeeds even with a wrong passphrase; the actual
   error only surfaces on the first real SQL query, producing an opaque
   worker-thread panic. Add an immediate PRAGMA user_version read to
   validate the key upfront with a clear error message.
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Addressed the two P1 findings from Greptile in 67e9062:

  1. Debug key exposure — replaced #[derive(Debug)] on DatastoreMethod with a manual Debug impl that renders the key as <redacted>.
  2. Silent wrong-key panic — added PRAGMA user_version read immediately after PRAGMA key to validate the passphrase upfront, with a clear error message.

The P2 (key zeroing via zeroize) is acknowledged and can be a follow-up.

1 similar comment
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Addressed the two P1 findings from Greptile in 67e9062:

  1. Debug key exposure — replaced #[derive(Debug)] on DatastoreMethod with a manual Debug impl that renders the key as <redacted>.
  2. Silent wrong-key panic — added PRAGMA user_version read immediately after PRAGMA key to validate the passphrase upfront, with a clear error message.

The P2 (key zeroing via zeroize) is acknowledged and can be a follow-up.

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw-server/src/main.rs
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

@greptileai review

@ErikBjare
Copy link
Copy Markdown
Member

@TimeToBuildBob See #584 (comment)

Per Erik's review on PR ActivityWatch#584: silently warning is wrong because the user
explicitly requested encryption — falling back to an unencrypted database
violates that intent. Better to refuse to start.

The user can either:
- rebuild with 'encryption' or 'encryption-vendored' feature, or
- unset AW_DB_PASSWORD to use an unencrypted database knowingly.
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

All review threads are now resolved. Summary of what was addressed since the initial Greptile review:

  • P1 (wrong-key panic) — fixed in 67e9062: added PRAGMA user_version read immediately after PRAGMA key to validate the passphrase upfront with a clear error message
  • P0 (Debug key exposure) — fixed in 67e9062: replaced #[derive(Debug)] with a manual impl that redacts the key as <redacted>
  • P1 (AW_DB_PASSWORD silently ignored) — fixed in 4597c2c: binary now panics at startup when the env var is set without encryption support compiled in
  • P2 (key zeroing) — acknowledged, deferred as a follow-up (tracked separately)

CI is green on all platforms (ubuntu, macOS, Windows, Android). Greptile 5/5. Ready for merge @ErikBjare — just needs your LGTM.

TimeToBuildBob added a commit to gptme/gptme-contrib that referenced this pull request Apr 25, 2026
…rikBjare/bob#682) (#753)

The permission-blocked merge_ready suppression added in #750 only matched four
canonical phrases (e.g. "waiting only on a maintainer click", "ready to merge
when convenient"). Bob's real-world ready comment on
ActivityWatch/aw-server-rust#584 was "Ready for merge @ErikBjare — just needs
your LGTM.", which fell through the suppression and re-dispatched as fake-ready
work.

Add a regex pattern matching "ready (to|for) merge @<maintainer>" — the
@-mention is the explicit maintainer-handoff signal that distinguishes a real
suppression case from generic "ready to merge once CI passes" prose.

Apply the same pattern to both `has_maintainer_waiting_comment` in
activity-gate.sh and `is_permission_blocked_merge_ready_pr` in
check-notifications.sh so both project-monitoring paths stay aligned.

Verified end-to-end against ActivityWatch/aw-server-rust#584: function now
returns SUPPRESSED.

Closes ErikBjare/bob#682
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Encryption at rest for database files

2 participants