docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,333 @@
<?php
declare(strict_types=1);
namespace App\Framework\Vault;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Http\IpAddress;
use App\Framework\Ulid\Ulid;
use App\Framework\UserAgent\UserAgent;
use App\Framework\Vault\Exceptions\VaultException;
use App\Framework\Vault\Exceptions\VaultKeyNotFoundException;
use App\Framework\Vault\ValueObjects\SecretKey;
use App\Framework\Vault\ValueObjects\SecretValue;
use App\Framework\Vault\ValueObjects\VaultAction;
use App\Framework\Vault\ValueObjects\VaultAuditEntry;
use App\Framework\Vault\ValueObjects\VaultMetadata;
use DateTimeImmutable;
/**
* Database-backed Vault Implementation
*
* Features:
* - Libsodium encryption (authenticated encryption)
* - Transactional safety
* - Audit logging
* - Metadata tracking
* - Key rotation support
*/
final readonly class DatabaseVault implements Vault
{
private const ENCRYPTION_VERSION = 1;
public function __construct(
private ConnectionInterface $connection,
private string $encryptionKey,
private VaultAuditLogger $auditLogger,
private ?IpAddress $clientIp = null,
private ?UserAgent $userAgent = null
) {
if (!extension_loaded('sodium')) {
throw new \RuntimeException('Sodium extension required for DatabaseVault');
}
if (strlen($this->encryptionKey) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new \InvalidArgumentException(
'Encryption key must be ' . SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes'
);
}
}
public function get(SecretKey $key): SecretValue
{
$row = $this->connection->queryOne(
'SELECT encrypted_value, encryption_nonce FROM vault_secrets WHERE secret_key = ?',
[$key->value]
);
if ($row === null) {
throw VaultKeyNotFoundException::forKey($key);
}
try {
$decrypted = $this->decrypt(
base64_decode($row['encrypted_value']),
base64_decode($row['encryption_nonce'])
);
// Update access metadata
$this->updateAccessMetadata($key);
// Audit Log
$this->auditLogger->log(
VaultAuditEntry::forRead($key, null, $this->clientIp, $this->userAgent)
);
return new SecretValue($decrypted);
} catch (\Throwable $e) {
$this->auditLogger->log(
new VaultAuditEntry(
key: $key,
action: VaultAction::READ,
timestamp: new DateTimeImmutable(),
ipAddress: $this->clientIp,
userAgent: $this->userAgent,
success: false,
errorMessage: $e->getMessage()
)
);
throw VaultException::decryptionFailed($e->getMessage());
}
}
public function set(SecretKey $key, SecretValue $value): void
{
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
try {
$encrypted = $this->encrypt($value->reveal(), $nonce);
$data = [
'id' => Ulid::generate(),
'secret_key' => $key->value,
'encrypted_value' => base64_encode($encrypted),
'encryption_nonce' => base64_encode($nonce),
'encryption_version' => self::ENCRYPTION_VERSION,
'created_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
'updated_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
];
// Upsert: Insert oder Update wenn bereits existiert
if ($this->has($key)) {
$this->connection->update(
'vault_secrets',
[
'encrypted_value' => $data['encrypted_value'],
'encryption_nonce' => $data['encryption_nonce'],
'updated_at' => $data['updated_at'],
],
['secret_key' => $key->value]
);
} else {
$this->connection->insert('vault_secrets', $data);
}
// Audit Log
$this->auditLogger->log(
VaultAuditEntry::forWrite($key, null, $this->clientIp, $this->userAgent)
);
} catch (\Throwable $e) {
$this->auditLogger->log(
new VaultAuditEntry(
key: $key,
action: VaultAction::WRITE,
timestamp: new DateTimeImmutable(),
ipAddress: $this->clientIp,
userAgent: $this->userAgent,
success: false,
errorMessage: $e->getMessage()
)
);
throw VaultException::encryptionFailed($e->getMessage());
}
}
public function has(SecretKey $key): bool
{
$result = $this->connection->queryOne(
'SELECT COUNT(*) as count FROM vault_secrets WHERE secret_key = ?',
[$key->value]
);
return ($result['count'] ?? 0) > 0;
}
public function delete(SecretKey $key): void
{
if (!$this->has($key)) {
throw VaultKeyNotFoundException::forKey($key);
}
$this->connection->delete(
'vault_secrets',
['secret_key' => $key->value]
);
// Audit Log
$this->auditLogger->log(
new VaultAuditEntry(
key: $key,
action: VaultAction::DELETE,
timestamp: new DateTimeImmutable(),
ipAddress: $this->clientIp,
userAgent: $this->userAgent
)
);
}
public function all(): array
{
$result = $this->connection->query(
'SELECT secret_key FROM vault_secrets ORDER BY secret_key ASC'
);
return array_map(
fn (array $row) => $row['secret_key'],
$result
);
}
public function getMetadata(SecretKey $key): VaultMetadata
{
$row = $this->connection->queryOne(
'SELECT * FROM vault_secrets WHERE secret_key = ?',
[$key->value]
);
if ($row === null) {
throw VaultKeyNotFoundException::forKey($key);
}
return new VaultMetadata(
key: $key,
createdAt: new DateTimeImmutable($row['created_at']),
updatedAt: new DateTimeImmutable($row['updated_at']),
createdBy: $row['created_by'],
updatedBy: $row['updated_by'],
accessCount: (int) $row['access_count'],
lastAccessedAt: $row['last_accessed_at']
? new DateTimeImmutable($row['last_accessed_at'])
: null
);
}
public function rotateEncryptionKey(string $newKey): int
{
if (strlen($newKey) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new \InvalidArgumentException(
'New encryption key must be ' . SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes'
);
}
$allSecrets = $this->connection->query(
'SELECT secret_key, encrypted_value, encryption_nonce FROM vault_secrets'
);
$rotatedCount = 0;
foreach ($allSecrets as $row) {
try {
// Decrypt mit altem Key
$decrypted = $this->decrypt(
base64_decode($row['encrypted_value']),
base64_decode($row['encryption_nonce'])
);
// Encrypt mit neuem Key
$newNonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$newEncrypted = sodium_crypto_secretbox(
$decrypted,
$newNonce,
$newKey
);
// Update in Database
$this->connection->update(
'vault_secrets',
[
'encrypted_value' => base64_encode($newEncrypted),
'encryption_nonce' => base64_encode($newNonce),
'updated_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
],
['secret_key' => $row['secret_key']]
);
$rotatedCount++;
// Audit Log
$this->auditLogger->log(
new VaultAuditEntry(
key: new SecretKey($row['secret_key']),
action: VaultAction::ROTATE,
timestamp: new DateTimeImmutable(),
ipAddress: $this->clientIp,
userAgent: $this->userAgent
)
);
} catch (\Throwable $e) {
// Log Error aber continue mit anderen Secrets
error_log("Failed to rotate secret '{$row['secret_key']}': {$e->getMessage()}");
}
}
return $rotatedCount;
}
private function encrypt(string $plaintext, string $nonce): string
{
return sodium_crypto_secretbox($plaintext, $nonce, $this->encryptionKey);
}
private function decrypt(string $encrypted, string $nonce): string
{
$decrypted = sodium_crypto_secretbox_open(
$encrypted,
$nonce,
$this->encryptionKey
);
if ($decrypted === false) {
throw VaultException::decryptionFailed('Invalid ciphertext or key');
}
return $decrypted;
}
private function updateAccessMetadata(SecretKey $key): void
{
$this->connection->query(
'UPDATE vault_secrets
SET access_count = access_count + 1,
last_accessed_at = ?
WHERE secret_key = ?',
[(new DateTimeImmutable())->format('Y-m-d H:i:s'), $key->value]
);
}
/**
* Generate a new encryption key for Vault
*/
public static function generateEncryptionKey(): string
{
return sodium_crypto_secretbox_keygen();
}
/**
* Encode encryption key für .env Storage
*/
public static function encodeKey(string $key): string
{
return base64_encode($key);
}
/**
* Decode encryption key von .env
*/
public static function decodeKey(string $encodedKey): string
{
return base64_decode($encodedKey);
}
}