docs(01-foundation-03): complete storage layer plan — SUMMARY, STATE, ROADMAP, REQUIREMENTS updated

- 01-03-SUMMARY.md: AES-256-GCM + Argon2id + SQLite CRUD layer complete
- STATE.md: progress 20%, decisions logged, session updated
- ROADMAP.md: Phase 1 In Progress (1/5 summaries)
- REQUIREMENTS.md: STOR-01, STOR-02, STOR-03 marked complete
This commit is contained in:
salvacybersec
2026-04-05 00:07:24 +03:00
parent 3334633867
commit 43aeb8985d
4 changed files with 168 additions and 8 deletions

View File

@@ -0,0 +1,139 @@
---
phase: 01-foundation
plan: 03
subsystem: database
tags: [sqlite, aes-256-gcm, argon2id, encryption, storage, modernc-sqlite]
# Dependency graph
requires:
- phase: 01-foundation-01
provides: go.mod with modernc.org/sqlite and golang.org/x/crypto dependencies
provides:
- AES-256-GCM column encryption (Encrypt/Decrypt) with random nonce prepended
- Argon2id key derivation (DeriveKey/NewSalt) using RFC 9106 parameters
- SQLite database Open() with WAL mode and embedded schema migration
- Finding CRUD: SaveFinding encrypts key_value at boundary, ListFindings decrypts transparently
- MaskKey helper: first8...last4 display format
- schema.sql with findings, scans, settings tables and performance indexes
affects: [01-04-scanner, 01-05-cli, 17-dashboard, 18-telegram]
# Tech tracking
tech-stack:
added:
- modernc.org/sqlite v1.48.1 (pure Go SQLite, CGO-free)
- golang.org/x/crypto (argon2.IDKey for key derivation)
- crypto/aes + crypto/cipher (stdlib AES-256-GCM)
patterns:
- "Encrypt-at-boundary: SaveFinding encrypts, ListFindings decrypts — storage layer handles all crypto transparently"
- "go:embed schema.sql — schema migrated on Open(), idempotent via CREATE TABLE IF NOT EXISTS"
- "WAL mode enabled on every Open() for concurrent read performance"
- "NULL scan_id: zero-value ScanID stored as SQL NULL to satisfy FK constraint"
key-files:
created:
- pkg/storage/encrypt.go
- pkg/storage/crypto.go
- pkg/storage/db.go
- pkg/storage/findings.go
- pkg/storage/schema.sql
- pkg/storage/db_test.go
modified: []
key-decisions:
- "Argon2id over PBKDF2: RFC 9106 recommended, memory-hard, resolves blocker from STATE.md"
- "NULL scan_id for findings without parent scan — FK constraint satisfied without mandatory scan creation"
- "Nonce prepended to ciphertext in single []byte — simplifies storage (no separate column needed)"
- "MaskKey returns first8...last4 — consistent with plan spec, 12-char minimum before masking"
patterns-established:
- "Pattern: Encrypt-at-boundary — pkg/storage is the only layer that sees encrypted bytes"
- "Pattern: sql.NullInt64 for nullable FK columns in scan results"
- "Pattern: go:embed for all embedded assets — schema.sql embedded in db.go"
requirements-completed: [STOR-01, STOR-02, STOR-03]
# Metrics
duration: 3min
completed: 2026-04-04
---
# Phase 1 Plan 3: Storage Layer Summary
**AES-256-GCM column encryption with Argon2id key derivation and SQLite CRUD — raw BLOB verified to contain no plaintext key data**
## Performance
- **Duration:** 3 min
- **Started:** 2026-04-04T21:02:00Z
- **Completed:** 2026-04-04T21:06:06Z
- **Tasks:** 2
- **Files modified:** 7 (5 created, go.mod + go.sum updated)
## Accomplishments
- AES-256-GCM Encrypt/Decrypt with prepended random nonce — non-deterministic, wrong-key fails with GCM auth error
- Argon2id DeriveKey using RFC 9106 Section 7.3 params (time=1, memory=64MB, threads=4, keyLen=32) — resolves the Argon2 vs PBKDF2 blocker from STATE.md
- SQLite opens with WAL mode, foreign keys, and embedded schema.sql migration — works with `:memory:` for tests
- SaveFinding/ListFindings transparently encrypt/decrypt key_value at storage boundary
- All 7 tests pass including raw-BLOB assertion confirming plaintext is not stored
## Task Commits
Each task was committed atomically:
1. **TDD RED: Failing test suite** - `2ef54f7` (test)
2. **Task 1: AES-256-GCM + Argon2id** - `239e2c2` (feat)
3. **Task 2: SQLite DB + schema + CRUD** - `3334633` (feat)
## Files Created/Modified
- `pkg/storage/encrypt.go` - Encrypt(plaintext, key) and Decrypt(ciphertext, key) using AES-256-GCM
- `pkg/storage/crypto.go` - DeriveKey(passphrase, salt) using Argon2id RFC 9106, NewSalt() 16-byte random
- `pkg/storage/db.go` - DB struct with Open(), Close(), SQL() — WAL mode, FK, embedded schema migration
- `pkg/storage/findings.go` - Finding struct, SaveFinding, ListFindings, MaskKey helper
- `pkg/storage/schema.sql` - CREATE TABLE for findings, scans, settings + 3 indexes
- `pkg/storage/db_test.go` - 7 tests including raw-BLOB encryption verification
## Decisions Made
- Argon2id selected over PBKDF2 (resolves STATE.md blocker) — memory-hard, RFC 9106 recommended
- NULL scan_id: zero-value ScanID stored as SQL NULL so findings can exist without a parent scan
- Single []byte for nonce+ciphertext — no separate nonce column needed, simplifies schema
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] FK constraint failed when ScanID = 0**
- **Found during:** Task 2 (TestSaveFindingEncrypted)
- **Issue:** Go zero value `ScanID: 0` sent as integer 0 to SQLite, failing FK constraint (no scan with id=0)
- **Fix:** SaveFinding converts zero ScanID to sql.NullInt64{} (NULL), ListFindings uses sql.NullInt64 for scan
- **Files modified:** pkg/storage/findings.go
- **Verification:** TestSaveFindingEncrypted passes after fix
- **Committed in:** 3334633 (Task 2 commit)
---
**Total deviations:** 1 auto-fixed (Rule 1 - Bug)
**Impact on plan:** Necessary correctness fix — plan spec allows NULL scan_id (no NOT NULL in schema). No scope creep.
## Issues Encountered
None beyond the FK constraint bug documented above.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Storage layer fully functional with transparent encryption
- pkg/storage exports: Encrypt, Decrypt, DeriveKey, NewSalt, Open, DB, Finding, SaveFinding, ListFindings, MaskKey
- Scanner (Plan 04) can call SaveFinding to persist findings
- CLI (Plan 05) can call ListFindings to display findings
---
*Phase: 01-foundation*
*Completed: 2026-04-04*