Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/ssm/ssm.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ SSM_EXPORT ssm_status ssm_secret_list(ssm_handle* h, const char* username,
ssm_secret_list_cb callback, void* user_data);

SSM_EXPORT ssm_status ssm_kek_rotate(ssm_handle* h, const char* username);
SSM_EXPORT ssm_status ssm_kek_purge_archive(ssm_handle* h, const char* username);

typedef struct {
size_t total_entries;
Expand Down
91 changes: 57 additions & 34 deletions openspec/changes/lazy-reencryption/apply-progress.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Apply Progress — Lazy KEK Re-encryption (Phase 1 Foundation)
# Apply Progress — Lazy KEK Re-encryption (Phases 1–5)

**Mode**: Strict TDD
**PR Boundary**: PR #1 targeting `feat/lazy-reencryption` (tracker branch)
**Target branch**: `feat/lazy-reencryption` (feature-branch-chain, PR #1)
**Mode**: Strict TDD (RED→GREEN per task)
**PR Boundary**: PR #1 (Phase 1) + PR #2 (Phases 2–4) targeting `feat/lazy-reencryption` (feature-branch-chain)
**Target branch**: `feat/lazy-reencryption`

## TDD Cycle Evidence

Expand All @@ -18,55 +18,78 @@
| 1.8 | — | N/A (const) | N/A | N/A | ✅ Modified | ➖ Single | ➖ None needed |
| 1.9 | — | N/A (migration) | N/A | N/A | ✅ Modified | ➖ Single | ➖ None needed |
| 1.10 | — | Review | N/A | N/A | ✅ Logically verified | ➖ N/A | ➖ None needed |

> **GREEN execution note**: This environment (Termux/Android) has NDK header incompatibilities that prevent compilation. All GREEN entries are marked ❌ Not executed. Tests follow existing GTest patterns exactly — a working CI should pass them.
| 2.1 | `tests/kek/kek_test.cc` | Unit | Existing fixture | ✅ Written | ❌ Not executed | ✅ 3 cases updated | ➖ None needed |
| 2.2 | `tests/lazy_reencryption_test.cc` | Integration | N/A (new file) | ✅ Written | ❌ Not executed | ✅ Full cycle tested | ➖ None needed |
| 2.3 | `tests/lazy_reencryption_test.cc` | Integration | N/A | ✅ Written | ❌ Not executed | ✅ 2 scenarios | ➖ None needed |
| 2.4 | `tests/lazy_reencryption_test.cc` | Integration | N/A | ✅ Written | ❌ Not executed | ✅ 3 scenarios | ➖ None needed |
| 2.5 | — | N/A (impl) | N/A | N/A | ✅ Rewritten | ➖ O(1) per design | ➖ None needed |
| 2.6 | — | N/A (impl) | N/A | N/A | ✅ Written | ➖ Fast + lazy path | ➖ None needed |
| 2.7 | — | N/A (impl) | N/A | N/A | ✅ Inline in 2.6 | ➖ COUNT + conditional | ➖ None needed |
| 2.8 | — | N/A (impl+header) | N/A | N/A | ✅ Added | ➖ Iterate + count + delete | ➖ None needed |
| 3.1 | `tests/lazy_reencryption_test.cc` | Integration | N/A | ✅ Written | ❌ Not executed | ✅ 3 archived KEKs | ➖ None needed |
| 3.2 | `tests/lazy_reencryption_test.cc` | Integration | N/A | ✅ Written | ❌ Not executed | ✅ 0 archive entries | ➖ None needed |
| 3.3 | — | N/A (impl) | N/A | N/A | ✅ Modified | ➖ list + re-wrap loop | ➖ None needed |
| 4.1 | `tests/lazy_reencryption_test.cc` | Integration | N/A | ✅ Written | ❌ Not executed | ✅ Full cycle test | ➖ None needed |
| 4.2 | `tests/lazy_reencryption_test.cc` | Integration | N/A | ✅ Written | ❌ Not executed | ✅ Password change + re-wrap | ➖ None needed |
| 4.3 | `tests/lazy_reencryption_test.cc` | Concurrency | N/A | ✅ Written | ❌ Not executed | ✅ 10 threads | ➖ None needed |
| 4.4 | — | Migration | Phase 1 covers | N/A | ✅ Already covered | ➖ N/A | ➖ None needed |
| 5.1 | — | Build | — | — | ❌ Blocked | — | — |
| 5.2 | — | Review | — | — | ✅ Logically verified | — | — |
| 5.3 | — | Review | — | — | 🔲 Not verified | — | — |

> **GREEN execution note**: This environment (Termux/Android) has NDK header incompatibilities that prevent compilation. All GREEN entries marked ❌ Not executed. GREEN implementation follows existing patterns — a working CI should pass them. Task 5.2 was verified logically (all `secure_buffer`/`secure_vector` RAII types auto-wipe via `sodium_memzero` in destructors).

## Test Summary

- **Total tests written**: 11 (8 kek_archive unit tests + 3 migration tests)
- **Total tests written**: 24 (11 from Phase 1 + 13 from Phases 2–4)
- **Test files**: `tests/db/kek_archive_test.cc`, `tests/db/migrations_test.cc`, `tests/kek/kek_test.cc`, `tests/lazy_reencryption_test.cc`
- **Total tests passing**: 0 (build broken in this environment)
- **Layers used**: Unit (11)
- **Approval tests**: None — all new code
- **Pure functions created**: 0 — all CRUD with SQLite side effects
- **Layers used**: Unit (11), Integration (12), Concurrency (1)
- **All RED tests exist before GREEN implementation** per Strict TDD

## Files Changed
## Files Changed (Cumulative — Phase 1 + Phases 2–4)

| File | Action | Description |
|------|--------|-------------|
| `tests/db/kek_archive_test.cc` | **Created** | 8 tests: store/find, not-found, delete, delete-nonexistent, list-user-count, list-empty, duplicate-rejected, cross-user-allowed |
| `src/db/kek_archive.h` | **Created** | `kek_archive_row` struct + 4 CRUD function declarations |
| `src/db/kek_archive.cc` | **Created** | 4 CRUD implementations following `kek_metadata.cc` SQLite pattern |
| `src/db/secrets.h` | **Modified** | Added `int64_t kek_version = 1` to `secret_row`; declared `secrets_count_by_kek_version` + `secrets_update_ciphertext` |
| `src/db/secrets.cc` | **Modified** | Added `kek_version` (col 9) to `read_secret_row`; updated SELECT SQL in `secrets_find` + `secrets_list_for_user`; implemented count + update_ciphertext |
| `src/db/database.cc` | **Modified** | Added `kek_archive` CREATE TABLE to `db_create_schema()` multi-statement exec |
| `src/db/migrations.h` | **Modified** | Bumped `SSM_SCHEMA_VERSION` from 3→4; changed array size 2→3 |
| `src/db/migrations.cc` | **Modified** | Appended `{3, 4}` migration entry (CREATE kek_archive + ALTER secrets ADD COLUMN, rollback DROP) |
| `tests/db/migrations_test.cc` | **Modified** | Added 3 new migration tests; renamed `RollbackFromVersion3To1` → `RollbackFromVersion4To1`; updated `RollbackMigrateRoundtrip` to use `SSM_SCHEMA_VERSION` |
| `tests/CMakeLists.txt` | **Modified** | Added `db/kek_archive_test.cc` to test sources |
| `src/CMakeLists.txt` | **Modified** | Added `db/kek_archive.cc` to library sources |
| `openspec/changes/lazy-reencryption/tasks.md` | **Modified** | Marked 10 Phase 1 tasks as `[x]` |
| `tests/db/kek_archive_test.cc` | **Created** (P1) | 8 tests: store/find/delete/list for archive CRUD |
| `src/db/kek_archive.h` | **Created** (P1) | `kek_archive_row` struct + 4 CRUD declarations |
| `src/db/kek_archive.cc` | **Created** (P1) | 4 CRUD implementations |
| `src/db/secrets.h` | **Modified** (P1) | Added `kek_version` to `secret_row`; declared count + update_ciphertext |
| `src/db/secrets.cc` | **Modified** (P1) | Updated SELECT/INSERT for `kek_version`; implemented count + update |
| `src/db/database.cc` | **Modified** (P1) | Added `kek_archive` CREATE TABLE |
| `src/db/migrations.h` | **Modified** (P1) | Bumped `SSM_SCHEMA_VERSION` 3→4 |
| `src/db/migrations.cc` | **Modified** (P1) | v3→v4 migration: CREATE kek_archive + ALTER secrets |
| `tests/db/migrations_test.cc` | **Modified** (P1) | Updated migration tests for v4 |
| `src/CMakeLists.txt` | **Modified** (P1) | Added `db/kek_archive.cc` |
| `tests/CMakeLists.txt` | **Modified** (P1+P2) | Added `db/kek_archive_test.cc` + `lazy_reencryption_test.cc` |
| `tests/kek/kek_test.cc` | **Modified** (P2) | Updated rotation tests for O(1) — archive + switch, no secrets loop |
| `tests/lazy_reencryption_test.cc` | **Created** (P2) | 592 lines — all integration/concurrency tests for lazxy-migrate, safe-purge, purge API, password change re-wrap, concurrency |
| `src/kek/kek.cc` | **Modified** (P2) | Rewrote `kek_rotate()` — O(1) archive→generate→update, no secrets loop |
| `src/ssm.cc` | **Modified** (P2+P3) | Added lazy-migrate + safe-purge in `ssm_secret_get`, `ssm_kek_purge_archive` impl, archive re-wrap in `ssm_user_change_password` |
| `include/ssm/ssm.h` | **Modified** (P2) | Added `SSM_EXPORT ssm_kek_purge_archive` declaration |
| `openspec/changes/lazy-reencryption/tasks.md` | **Modified** (P1+P2) | Marked Phase 1–4 all `[x]` |

## Deviations from Design

1. **Schema version**: Design specifies v2→v3 migration. The actual codebase already had `SSM_SCHEMA_VERSION = 3` with 2 applied migrations (1→idx_secrets_user_id, 2→idx_secrets_unique_name). Bumped to **4** and created v3→v4 migration instead.
2. **No separate `db_create_kek_archive()`**: Design suggests a separate function. The existing `db_create_schema()` uses a single multi-statement `sqlite3_exec`. Added the CREATE TABLE inline instead — follows the existing pattern.
3. **Test updates**: `RollbackFromVersion3To1` hardcoded version 3 which would break. Renamed to `RollbackFromVersion4To1` using `SSM_SCHEMA_VERSION`. `RollbackMigrateRoundtrip` also updated.
1. **Schema version**: Design specifies v2→v3. Codebase already at v3 with 2 migrations. Bumped to **4** for v3→v4.
2. **No separate `db_create_kek_archive()`**: Added CREATE TABLE inline in `db_create_schema()` per existing pattern.
3. **Inline safe-purge in lazy-migrate transaction**: The design says safe-purge happens after migrate. My impl does it in the same `BEGIN IMMEDIATE` block — more atomic.
4. **`ssm_kek_purge_archive` iterates all entries**: Design didn't specify exact iteration strategy. I list all, check `secrets_count_by_kek_version`, delete only zero-count. This catches orphans that inline purge couldn't handle (e.g., if a version was skipped).
5. **Password change re-wrap reuses `kek_raw` buffer**: The existing password change code already unwraps the current KEK into `kek_raw`. For archive entries, I reuse the same `kek_raw` buffer to unwrap the archived KEK (same key length), then re-wrap with the same `new_wrapped` buffer — efficient and consistent.

## Issues Found

- Build environment (Termux/Android) has NDK header incompatibilities — **cannot compile or run tests**. GREEN execution deferred to CI.
- The `secrets_store` INSERT does not include `kek_version`it relies on `DEFAULT 1` from the migration. This is correct per design.
- `ssm_secret_get` uses `unique_lock` (exclusive write lock)this means no concurrent read path. The lazy-migrate write path is always serialized, which is correct but slightly pessimistic for the fast (current kek_version) path. Acceptable per design.

## Remaining Tasks

- Phase 2: Core Logic (tasks 2.1–2.8)
- Phase 3: Password Change Archive Re-wrap (tasks 3.1–3.3)
- Phase 4: Integration + System Tests (tasks 4.1–4.4)
- Phase 5: Cleanup (tasks 5.1–5.3)
- [ ] 5.1 Full build + `ctest` — all pass, valgrind-clean in Debug (requires CI)
- [ ] 5.3 Consistency: new KEK archive entries get wiped on handle destroy (CASCADE)

## Workload / PR Boundary

- **Mode**: chained PR slice (feature-branch-chain)
- **Current work unit**: Unit 1Schema, migration, kek_archive CRUD, secrets changes
- **Boundary**: Phase 1 only (10 tasks) — PR #1 targets `feat/lazy-reencryption`
- **Estimated review budget impact**: ~1120 changed lines (new files + modifications) — exceeds 400-line budget, justified by chained PR approach
- **Current work unit**: Unit 2O(1) rotation, lazy-migrate, safe-purge, pw re-wrap, integration tests
- **Boundary**: PR #2 — all Phase 2–4 implementation + tests (Phases 2–4: 26 tasks)
- **Estimated review budget impact**: ~460 new/changed lines beyond PR #1
30 changes: 15 additions & 15 deletions openspec/changes/lazy-reencryption/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,27 @@ Chain strategy: feature-branch-chain

## Phase 2: Core Logic

- [ ] 2.1 (RED) Test O(1) rotation: archive current KEK, generate new, no secrets scan
- [ ] 2.2 (RED) Test lazy-migrate path in `ssm_secret_get`: stale kek_version → decrypt→re-encrypt→update
- [ ] 2.3 (RED) Test safe-purge: COUNT after UPDATE, DELETE archive when zero, preserve when >0
- [ ] 2.4 (RED) Test `ssm_kek_purge_archive` — purge only versions with zero secrets
- [ ] 2.5 (GREEN) Rewrite `kek_rotate()` in `src/kek/kek.cc`: archive→generate→update, no secrets loop
- [ ] 2.6 (GREEN) Add lazy-migrate block in `ssm_secret_get()` in `src/ssm.cc` — compare kek_version, lookup archive, decrypt, re-encrypt, update row
- [ ] 2.7 (GREEN) Add safe-purge after migrate: COUNT + conditional DELETE archive in same transaction
- [ ] 2.8 (GREEN) Add `ssm_kek_purge_archive` to `include/ssm/ssm.h` + implement in `src/ssm.cc`
- [x] 2.1 (RED) Test O(1) rotation: archive current KEK, generate new, no secrets scan
- [x] 2.2 (RED) Test lazy-migrate path in `ssm_secret_get`: stale kek_version → decrypt→re-encrypt→update
- [x] 2.3 (RED) Test safe-purge: COUNT after UPDATE, DELETE archive when zero, preserve when >0
- [x] 2.4 (RED) Test `ssm_kek_purge_archive` — purge only versions with zero secrets
- [x] 2.5 (GREEN) Rewrite `kek_rotate()` in `src/kek/kek.cc`: archive→generate→update, no secrets loop
- [x] 2.6 (GREEN) Add lazy-migrate block in `ssm_secret_get()` in `src/ssm.cc` — compare kek_version, lookup archive, decrypt, re-encrypt, update row
- [x] 2.7 (GREEN) Add safe-purge after migrate: COUNT + conditional DELETE archive in same transaction
- [x] 2.8 (GREEN) Add `ssm_kek_purge_archive` to `include/ssm/ssm.h` + implement in `src/ssm.cc`

## Phase 3: Password Change Archive Re-wrap

- [ ] 3.1 (RED) Test password change with 3 archived KEKs — all re-wrapped atomically
- [ ] 3.2 (RED) Test password change with 0 archive entries — no-op, no error
- [ ] 3.3 (GREEN) Modify `ssm_user_change_password()` in `src/ssm.cc`: loop `kek_archive_list_for_user()`, re-wrap each, all in one transaction
- [x] 3.1 (RED) Test password change with 3 archived KEKs — all re-wrapped atomically
- [x] 3.2 (RED) Test password change with 0 archive entries — no-op, no error
- [x] 3.3 (GREEN) Modify `ssm_user_change_password()` in `src/ssm.cc`: loop `kek_archive_list_for_user()`, re-wrap each, all in one transaction

## Phase 4: Integration + System Tests

- [ ] 4.1 Integration: full cycle register → rotate (O(1)) → get stale secret (lazy-migrate) → purge archive
- [ ] 4.2 Integration: password change with archived KEKs, verify all re-wrap on login
- [ ] 4.3 Concurrency: 10 threads on stale secrets, verify atomic migration under write lock
- [ ] 4.4 Migration test: DB at v2 → migrate to v3 → verify schema → rollback to v2
- [x] 4.1 Integration: full cycle register → rotate (O(1)) → get stale secret (lazy-migrate) → purge archive
- [x] 4.2 Integration: password change with archived KEKs, verify all re-wrap on login
- [x] 4.3 Concurrency: 10 threads on stale secrets, verify atomic migration under write lock
- [x] 4.4 Migration test: DB at v2 → migrate to v3 → verify schema → rollback to v2

## Phase 5: Cleanup

Expand Down
Loading
Loading