Skip to content

app-storage: ENCLAVEAPP_MOCK_STORAGE env var forces mock backend#71

Merged
jgowdy-godaddy merged 1 commit intomainfrom
feat/mock-storage-env-var
Apr 17, 2026
Merged

app-storage: ENCLAVEAPP_MOCK_STORAGE env var forces mock backend#71
jgowdy-godaddy merged 1 commit intomainfrom
feat/mock-storage-env-var

Conversation

@jgowdy-godaddy
Copy link
Copy Markdown
Contributor

Summary

Follow-up to #68/#69/#70. Those preserved the real-hardware path but couldn't solve the root cause of the npmenc macOS CI hang: unsigned binaries writing to the runner's login keychain block on an ACL confirmation prompt the headless runner cannot answer.

This PR adds the escape hatch the memory rule ("fix the test to use mocks; don't work around by skipping") has been calling for from the start — a runtime env var that routes `create_encryption_storage` through the existing `MockEncryptionStorage` instead of the real platform backend.

  • `ENCLAVEAPP_MOCK_STORAGE=1` → mock. Empty/unset → real backend (unchanged).
  • Mock is an in-memory AES-256-GCM key. Test/CI only; not production-safe. We log a warning whenever it activates so no one trips it accidentally.
  • Library-level keychain tests in `enclaveapp-apple` still exercise the real keychain path on CI — only consumer integration suites that would otherwise hang will opt in.

Mechanical changes

  • Move the mock module off the `mock` feature gate; compile it unconditionally. Cost is free since `aes-gcm`/`rand` are already in the graph.
  • Re-export `MockEncryptionStorage` for downstream use.
  • Drop the now-redundant `mock` feature from Cargo.toml.

Test plan

  • `cargo build --workspace`
  • `cargo clippy --workspace --all-targets -- -D warnings`
  • `cargo test --workspace` — all green locally
  • npmenc CI green on macOS when the workflow sets `ENCLAVEAPP_MOCK_STORAGE=1` for the Test step

Adds a runtime escape hatch for environments that cannot satisfy a
real hardware-backed backend. When ENCLAVEAPP_MOCK_STORAGE is set to
a non-empty value, create_encryption_storage returns a fresh
MockEncryptionStorage instead of initialising AppEncryptionStorage.

Primary use case: GitHub Actions macOS runners running integration
tests that would otherwise block indefinitely on a login-keychain
ACL confirmation prompt the headless runner cannot answer. The
library tests in enclaveapp-apple still exercise the real keychain
path locally — this only affects consumer integration tests that
opt in via the env var.

Mechanical changes:
- Move the mock module off the `mock` feature gate and compile it
  unconditionally; the aes-gcm + rand cost is already paid by the
  dependency graph.
- Re-export MockEncryptionStorage for downstream consumers.
- Drop the now-redundant `mock` feature from Cargo.toml.
@jgowdy-godaddy jgowdy-godaddy merged commit 43a6ff1 into main Apr 17, 2026
3 checks passed
jgowdy-godaddy pushed a commit that referenced this pull request Apr 17, 2026
The ENCLAVEAPP_MOCK_STORAGE path added in #71 constructed a fresh
MockEncryptionStorage via `::new()`, which generated a random
AES-256 key. That breaks any test suite whose parent `cargo test`
process writes encrypted state and then spawns a child binary
(e.g. via `assert_cmd::cargo_bin(...)`) that reads it back — the
child's `MockEncryptionStorage` has a different random key and
`decrypt` fails with `aead::Error`.

Derive the mock key deterministically from the app name:
`SHA-256("enclaveapp-app-storage mock v1\0" || app_name)`. Every
process instantiating storage for the same app now lands on the
same AES key, so cross-process tests succeed. The derivation is
explicitly non-secret — anyone with the app name can recompute
it — which is fine because the mock is test-only.

The existing `::new()` / `::default()` random path stays for
consumers that want that semantics, and the
`decrypt_wrong_key_fails` test still covers it.
jgowdy-godaddy added a commit that referenced this pull request Apr 17, 2026
The ENCLAVEAPP_MOCK_STORAGE path added in #71 constructed a fresh
MockEncryptionStorage via `::new()`, which generated a random
AES-256 key. That breaks any test suite whose parent `cargo test`
process writes encrypted state and then spawns a child binary
(e.g. via `assert_cmd::cargo_bin(...)`) that reads it back — the
child's `MockEncryptionStorage` has a different random key and
`decrypt` fails with `aead::Error`.

Derive the mock key deterministically from the app name:
`SHA-256("enclaveapp-app-storage mock v1\0" || app_name)`. Every
process instantiating storage for the same app now lands on the
same AES key, so cross-process tests succeed. The derivation is
explicitly non-secret — anyone with the app name can recompute
it — which is fine because the mock is test-only.

The existing `::new()` / `::default()` random path stays for
consumers that want that semantics, and the
`decrypt_wrong_key_fails` test still covers it.

Co-authored-by: Jay Gowdy <jay@gowdy.me>
jgowdy-godaddy pushed a commit that referenced this pull request Apr 17, 2026
#71 made the mock backend always-compiled so ENCLAVEAPP_MOCK_STORAGE
could be honored at runtime without cargo feature gymnastics. That
is a security hole for release binaries: anyone setting the env var
on a production install would downgrade the hardware-backed backend
to an AES-in-RAM mock.

Restore the compile-time gate:
- `mock` cargo feature opt-in, with aes-gcm/rand/sha2 as optional
  deps only pulled in by the feature.
- `mock` module, `MockEncryptionStorage` re-export, `MOCK_STORAGE_ENV`
  constant, and the env-var check inside `create_encryption_storage`
  all gated on `#[cfg(feature = "mock")]`.
- Release builds (no --features mock) have no path to the mock at
  all — setting ENCLAVEAPP_MOCK_STORAGE in production is a no-op.
- Test builds enable the feature via downstream `[dev-dependencies]`
  with `features = ["mock"]`; cargo unifies features during
  `cargo test`, so the env var works in CI as intended.

Keeps #72's deterministic key derivation inside the gated mock
module so cross-process test spawning still works.
jgowdy-godaddy added a commit that referenced this pull request Apr 17, 2026
#71 made the mock backend always-compiled so ENCLAVEAPP_MOCK_STORAGE
could be honored at runtime without cargo feature gymnastics. That
is a security hole for release binaries: anyone setting the env var
on a production install would downgrade the hardware-backed backend
to an AES-in-RAM mock.

Restore the compile-time gate:
- `mock` cargo feature opt-in, with aes-gcm/rand/sha2 as optional
  deps only pulled in by the feature.
- `mock` module, `MockEncryptionStorage` re-export, `MOCK_STORAGE_ENV`
  constant, and the env-var check inside `create_encryption_storage`
  all gated on `#[cfg(feature = "mock")]`.
- Release builds (no --features mock) have no path to the mock at
  all — setting ENCLAVEAPP_MOCK_STORAGE in production is a no-op.
- Test builds enable the feature via downstream `[dev-dependencies]`
  with `features = ["mock"]`; cargo unifies features during
  `cargo test`, so the env var works in CI as intended.

Keeps #72's deterministic key derivation inside the gated mock
module so cross-process test spawning still works.

Co-authored-by: Jay Gowdy <jay@gowdy.me>
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.

2 participants