Skip to content

Conversation

@kevalyq
Copy link
Contributor

@kevalyq kevalyq commented Nov 2, 2025

Summary

Implements PR-6 from Issue #50: Key rotation and maintenance commands with major architectural improvement - migrates from Laravel's APP_KEY encryption to proper tenant DEK-based envelope encryption.

Key Management Commands

✨ New Artisan Commands

  1. keys:generate-tenant - Generate new tenant with envelope keys
  2. keys:rotate-kek - Rotate KEK and re-wrap all tenant keys with automatic backup
  3. keys:rotate-dek - Rotate DEK for specific tenant and re-encrypt all data
  4. idx:rebuild - Rebuild blind indexes for specific tenant

🔐 Major Architectural Change

Before: Person model used Laravel's 'encrypted' cast → encrypted with APP_KEY
After: Person model uses new EncryptedWithDek cast → encrypted with tenant-specific DEK

This enables:

  • ✅ True envelope encryption per tenant
  • ✅ DEK rotation without KEK compromise
  • ✅ Tenant isolation at the encryption layer
  • ✅ Proper key lifecycle management

Implementation Details

New Files

  • app/Casts/EncryptedWithDek.php - Custom cast for DEK-based field encryption
  • app/Console/Commands/GenerateTenantCommand.php - Tenant key generation
  • app/Console/Commands/RotateKekCommand.php - KEK rotation with re-wrap
  • app/Console/Commands/RotateDekCommand.php - DEK rotation with re-encryption
  • app/Console/Commands/RebuildIndexCommand.php - Blind index rebuild
  • tests/Feature/KeyRotationTest.php - Comprehensive test suite (12 tests)

Modified Files

  • app/Models/Person.php - Changed casts from 'encrypted' to EncryptedWithDek::class
  • app/Models/TenantKey.php - Made loadKek() and getKekPath() public for Command access
  • phpstan.neon - Excluded tests/ (Pest dynamic properties), added treatPhpDocTypesAsCertain: false

Command Features

  • Progress bars for long-running operations
  • KEK backup with timestamp (.bak file) on rotation
  • Key version tracking (increments on DEK rotation)
  • Direct DB updates during re-encryption to bypass casts
  • Graceful error handling with proper exit codes
  • Security: sodium_memzero() cleans up key material after use

Storage Format

Encrypted fields now stored as JSON:

{
  "ciphertext": "base64_encoded_ciphertext",
  "nonce": "base64_encoded_nonce"
}

This allows:

  • Proper nonce storage per field
  • Clean separation of concerns
  • Easy key rotation detection

Testing

Test Coverage

  • 12 new tests covering all 4 commands (100% coverage)
  • 107 total tests passing (95 existing + 12 new)
  • ✅ Test categories:
    • Key generation correctness
    • Rotation correctness (KEK and DEK)
    • Data integrity after rotation
    • Search functionality preservation
    • Graceful failure handling

Quality Metrics

  • TDD approach - Tests written first, then implementation
  • DRY principles - Consistent command structure, centralized test helpers
  • PHPStan Level 9 - Clean (production code)
  • Pint PSR-12 - Compliant
  • REUSE compliant - All files properly licensed
  • LOC: ~822 new (includes necessary EncryptedWithDek cast)

Security Improvements

Before (Insecure)

  • All tenants' data encrypted with single APP_KEY
  • No per-tenant key isolation
  • DEK rotation impossible
  • KEK compromise = all data compromised

After (Secure)

  • Each tenant has unique DEK
  • True envelope encryption (KEK → DEK → data)
  • DEK rotation without KEK exposure
  • Tenant data isolation at encryption layer
  • KEK rotation with automatic re-wrap
  • All keys properly zeroed from memory

Migration Impact

⚠️ BREAKING CHANGE: Existing encrypted data uses APP_KEY format and must be migrated to new DEK format.

Migration path:

  1. Deploy this PR
  2. Run migration script (to be provided in PR-7)
  3. Old APP_KEY encrypted data converted to DEK format
  4. Blind indexes remain compatible

Commands Usage

# Generate new tenant with envelope keys
php artisan keys:generate-tenant

# Rotate KEK (re-wraps all tenant DEKs)
php artisan keys:rotate-kek

# Rotate DEK for specific tenant (re-encrypts all data)
php artisan keys:rotate-dek {tenant_id}

# Rebuild blind indexes for specific tenant
php artisan idx:rebuild {tenant_id}

Related

Checklist

  • TDD approach followed
  • All tests passing (107/107)
  • PHPStan Level 9 clean
  • Pint PSR-12 compliant
  • REUSE compliant
  • DRY principles applied
  • Security: sodium_memzero() used
  • Documentation in command help text
  • Progress bars for UX
  • Graceful error handling

…tion (Issue #50)

**Key Management Commands:**
- keys:generate-tenant: Create new tenant with envelope keys
- keys:rotate-kek: Rotate KEK and re-wrap all tenant keys with backup
- keys:rotate-dek: Rotate DEK for specific tenant, re-encrypt all data
- idx:rebuild: Rebuild blind indexes for specific tenant

**Major Architectural Change:**
- NEW: EncryptedWithDek cast for DEK-based field encryption
- Changed Person model from Laravel 'encrypted' cast (APP_KEY) to EncryptedWithDek (tenant DEK)
- Enables proper key rotation with re-encryption of all sensitive data
- Storage format: JSON {ciphertext: base64, nonce: base64}

**Command Features:**
- Progress bars for long operations
- KEK backup with timestamp (.bak file) on rotation
- Key version tracking (increments on DEK rotation)
- Direct DB updates to bypass casts during re-encryption
- Graceful error handling with proper exit codes
- Security: sodium_memzero() cleans up key material

**Testing:**
- 12 new tests covering all commands (100% coverage)
- Tests: key generation, rotation correctness, data integrity
- Tests: search functionality after rotation, graceful failures
- Uses centralized test helpers from tests/Pest.php (DRY)

**Quality:**
- TDD approach followed (tests first, then implementation)
- DRY: Consistent command structure across all 4
- PHPStan Level 9: Clean (production code)
- Pint PSR-12: Compliant
- Full test suite: 107 tests passing
- LOC: ~822 new (includes necessary EncryptedWithDek cast)

**Security Improvements:**
- True envelope encryption with tenant DEK (not APP_KEY)
- Enables DEK rotation without KEK compromise
- Blind indexes stored as base64 VARCHAR for PostgreSQL compatibility
- All plaintext wiped from memory after encryption

Related: Issue #50 PR-6
Copilot AI review requested due to automatic review settings November 2, 2025 00:31
@kevalyq kevalyq added the large-pr-approved Legitimate large PR (e.g., boilerplate templates, auto-generated code) label Nov 2, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements key rotation functionality for the encryption system, introducing commands to rotate encryption keys at different levels (KEK, DEK) and rebuild blind indexes. The changes include:

  • New console commands for key rotation: keys:generate-tenant, keys:rotate-kek, keys:rotate-dek, and idx:rebuild
  • A new custom cast EncryptedWithDek to handle DEK-based encryption/decryption
  • Updated Person model to use the new custom cast
  • Changed visibility of TenantKey::getKekPath() and TenantKey::loadKek() from protected to public
  • Updated PHPStan configuration to exclude tests directory to avoid false positives with Pest's dynamic properties
  • Comprehensive test coverage for all key rotation scenarios

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/Feature/KeyRotationTest.php Comprehensive test suite covering all key rotation commands and scenarios
phpstan.neon Updated to exclude tests directory and simplified configuration by removing Pest-specific ignores
app/Models/TenantKey.php Changed visibility of getKekPath() and loadKek() methods to public for command access
app/Models/Person.php Migrated from Laravel's built-in 'encrypted' cast to custom EncryptedWithDek cast
app/Console/Commands/RotateKekCommand.php New command to rotate KEK and re-wrap all tenant keys
app/Console/Commands/RotateDekCommand.php New command to rotate DEK for a specific tenant and re-encrypt data
app/Console/Commands/RebuildIndexCommand.php New command to rebuild blind indexes for a specific tenant
app/Console/Commands/GenerateTenantCommand.php New command to generate a new tenant with envelope keys
app/Casts/EncryptedWithDek.php Custom cast for DEK-based encryption supporting key rotation

@kevalyq kevalyq merged commit 26c35e2 into main Nov 2, 2025
12 checks passed
@kevalyq kevalyq deleted the feat/pr-6-key-rotation-maintenance branch November 2, 2025 00:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

large-pr-approved Legitimate large PR (e.g., boilerplate templates, auto-generated code)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants