Files
michaelschiemer/docs/components/cryptography/examples.md
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

21 KiB

Cryptography Module - Praktische Beispiele

Umfassende Sammlung praktischer Anwendungsfälle für das Cryptography Module.

Authentifizierung und Session-Management

User Password System

final readonly class UserAuthenticationService
{
    public function __construct(
        private KeyDerivationFunction $kdf,
        private SecureTokenGenerator $tokenGenerator,
        private UserRepository $userRepository
    ) {}
    
    public function registerUser(string $email, string $password): User
    {
        // Password hashen mit Argon2ID
        $hashedPassword = $this->kdf->hashPassword($password, 'argon2id', [
            'memory_cost' => 65536,  // 64 MB
            'time_cost' => 4,        // 4 Iterationen
            'threads' => 3           // 3 Threads
        ]);
        
        $user = new User(
            id: Ulid::generate(),
            email: new Email($email),
            passwordHash: $hashedPassword,
            createdAt: new DateTimeImmutable()
        );
        
        return $this->userRepository->save($user);
    }
    
    public function authenticateUser(string $email, string $password): ?SessionToken
    {
        $user = $this->userRepository->findByEmail(new Email($email));
        
        if (!$user || !$this->kdf->verify($password, $user->getPasswordHash())) {
            // Timing-Angriffe verhindern durch konstante Ausführungszeit
            $this->kdf->hashPassword('dummy-password', 'argon2id');
            return null;
        }
        
        // Session Token generieren
        $sessionToken = $this->tokenGenerator->generateSessionToken(48); // 384 Bit
        
        // In Session Store speichern
        $this->storeSessionToken($user, $sessionToken);
        
        return $sessionToken;
    }
    
    private function storeSessionToken(User $user, SecureToken $token): void
    {
        $session = new UserSession(
            tokenHash: hash('sha256', $token->getValue()),
            userId: $user->getId(),
            expiresAt: (new DateTimeImmutable())->add(new DateInterval('PT24H')),
            metadata: $token->getMetadata()
        );
        
        // Session in Datenbank speichern (nur Hash, nie Klartext!)
        $this->sessionRepository->save($session);
    }
}

API Key Management System

final readonly class ApiKeyManagementService
{
    public function __construct(
        private SecureTokenGenerator $tokenGenerator,
        private CryptographicUtilities $utils,
        private ApiKeyRepository $repository
    ) {}
    
    public function createApiKey(
        User $user, 
        string $name, 
        array $scopes = [],
        ?DateTimeImmutable $expiresAt = null
    ): ApiKeyResult {
        // API Key mit Prefix generieren
        $apiKey = $this->tokenGenerator->generateApiKey(
            prefix: 'ak',
            length: 32
        );
        
        // API Key Entity erstellen (Hash speichern, nicht Klartext)
        $apiKeyEntity = new ApiKey(
            id: Ulid::generate(),
            userId: $user->getId(),
            name: $name,
            keyHash: hash('sha256', $apiKey->getValue()),
            fingerprint: $apiKey->getFingerprint(),
            scopes: $scopes,
            expiresAt: $expiresAt,
            createdAt: new DateTimeImmutable()
        );
        
        $this->repository->save($apiKeyEntity);
        
        return new ApiKeyResult(
            apiKey: $apiKey->getValue(),        // Nur einmal zurückgeben
            keyId: $apiKeyEntity->getId(),
            fingerprint: $apiKey->getShortFingerprint(),
            expiresAt: $expiresAt
        );
    }
    
    public function validateApiKey(string $apiKey): ?ApiKey
    {
        $keyHash = hash('sha256', $apiKey);
        $storedKey = $this->repository->findByHash($keyHash);
        
        if (!$storedKey) {
            return null;
        }
        
        // Timing-sicherer Vergleich
        if (!$this->utils->timingSafeEquals($keyHash, $storedKey->getKeyHash())) {
            return null;
        }
        
        // Expiration prüfen
        if ($storedKey->getExpiresAt() && $storedKey->getExpiresAt() < new DateTimeImmutable()) {
            return null;
        }
        
        return $storedKey;
    }
    
    public function rotateApiKey(string $currentApiKey): ApiKeyResult
    {
        $oldKey = $this->validateApiKey($currentApiKey);
        
        if (!$oldKey) {
            throw new ApiKeyException('Invalid API key for rotation');
        }
        
        // Neuen Key mit gleichen Eigenschaften erstellen
        $newApiKey = $this->createApiKey(
            user: $oldKey->getUser(),
            name: $oldKey->getName() . ' (rotated)',
            scopes: $oldKey->getScopes(),
            expiresAt: $oldKey->getExpiresAt()
        );
        
        // Alten Key deaktivieren (nicht löschen für Audit-Trail)
        $oldKey->deactivate();
        $this->repository->save($oldKey);
        
        return $newApiKey;
    }
}

CSRF Protection

CSRF Token System

final readonly class CsrfProtectionService
{
    public function __construct(
        private SecureTokenGenerator $tokenGenerator,
        private CacheInterface $cache
    ) {}
    
    public function generateCsrfToken(string $sessionId): SecureToken
    {
        $csrfToken = $this->tokenGenerator->generateCsrfToken();
        
        // Token mit Session verknüpfen
        $cacheKey = "csrf_token_{$sessionId}";
        $this->cache->set($cacheKey, $csrfToken->getValue(), 3600); // 1 Stunde
        
        return $csrfToken;
    }
    
    public function validateCsrfToken(string $sessionId, string $providedToken): bool
    {
        $cacheKey = "csrf_token_{$sessionId}";
        $storedToken = $this->cache->get($cacheKey);
        
        if (!$storedToken) {
            return false;
        }
        
        // Timing-sicherer Vergleich
        $isValid = hash_equals($storedToken, $providedToken);
        
        // Token nach Verwendung löschen (Single-Use)
        if ($isValid) {
            $this->cache->delete($cacheKey);
        }
        
        return $isValid;
    }
}

// CSRF Middleware
final readonly class CsrfProtectionMiddleware
{
    public function __construct(
        private CsrfProtectionService $csrfService
    ) {}
    
    public function handle(HttpRequest $request, RequestHandler $handler): HttpResponse
    {
        // Nur bei state-changing Operations prüfen
        if (in_array($request->method, [Method::POST, Method::PUT, Method::DELETE, Method::PATCH])) {
            $sessionId = $request->session->getId();
            $csrfToken = $request->parsedBody->get('_token') ?? $request->headers->get('X-CSRF-Token');
            
            if (!$csrfToken || !$this->csrfService->validateCsrfToken($sessionId, $csrfToken)) {
                throw new CsrfTokenMismatchException('CSRF token mismatch');
            }
        }
        
        return $handler->handle($request);
    }
}

Webhook Security

Webhook Signature System

final readonly class WebhookSecurityService
{
    public function __construct(
        private SecureTokenGenerator $tokenGenerator,
        private CryptographicUtilities $utils
    ) {}
    
    public function generateWebhookSecret(): SecureToken
    {
        return $this->tokenGenerator->generateWebhookToken();
    }
    
    public function signWebhookPayload(string $payload, SecureToken $secret): string
    {
        $signature = hash_hmac('sha256', $payload, $secret->getValue());
        return 'sha256=' . $signature;
    }
    
    public function validateWebhookSignature(
        string $payload, 
        string $signature, 
        SecureToken $secret
    ): bool {
        $expectedSignature = $this->signWebhookPayload($payload, $secret);
        
        // Timing-sicherer Vergleich
        return $this->utils->timingSafeEquals($signature, $expectedSignature);
    }
}

// Webhook Handler
final readonly class PaymentWebhookHandler
{
    public function __construct(
        private WebhookSecurityService $security,
        private WebhookConfigRepository $configRepository
    ) {}
    
    #[Route(path: '/webhooks/payment', method: Method::POST)]
    public function handlePaymentWebhook(HttpRequest $request): JsonResult
    {
        $signature = $request->headers->get('X-Hub-Signature-256');
        $payload = $request->rawBody();
        
        if (!$signature) {
            throw new WebhookException('Missing signature header');
        }
        
        // Webhook Secret für den Sender abrufen
        $senderId = $request->headers->get('X-Sender-ID');
        $config = $this->configRepository->findBySenderId($senderId);
        
        if (!$config) {
            throw new WebhookException('Unknown sender');
        }
        
        // Signatur validieren
        if (!$this->security->validateWebhookSignature($payload, $signature, $config->getSecret())) {
            throw new WebhookException('Invalid signature');
        }
        
        // Payload verarbeiten
        $data = json_decode($payload, true);
        $this->processPaymentEvent($data);
        
        return new JsonResult(['status' => 'processed']);
    }
}

Two-Factor Authentication (2FA)

OTP-basiertes 2FA System

final readonly class TwoFactorAuthService
{
    public function __construct(
        private SecureTokenGenerator $tokenGenerator,
        private CacheInterface $cache,
        private NotificationService $notificationService
    ) {}
    
    public function generateOtpCode(User $user): string
    {
        $otpToken = $this->tokenGenerator->generateOtpToken(6); // 6-stelliger Code
        
        // OTP mit User-ID und Expiration speichern
        $cacheKey = "otp_{$user->getId()}";
        $otpData = [
            'code' => $otpToken->getValue(),
            'attempts' => 0,
            'created_at' => time()
        ];
        
        $this->cache->set($cacheKey, $otpData, 300); // 5 Minuten gültig
        
        return $otpToken->getValue();
    }
    
    public function sendOtpCode(User $user): void
    {
        $otpCode = $this->generateOtpCode($user);
        
        // SMS oder Email versenden
        $this->notificationService->sendOtpCode(
            recipient: $user->getPhoneNumber() ?? $user->getEmail(),
            code: $otpCode,
            expiresInMinutes: 5
        );
    }
    
    public function verifyOtpCode(User $user, string $providedCode): bool
    {
        $cacheKey = "otp_{$user->getId()}";
        $otpData = $this->cache->get($cacheKey);
        
        if (!$otpData) {
            return false; // Kein OTP vorhanden oder abgelaufen
        }
        
        // Rate-Limiting: Max 3 Versuche
        if ($otpData['attempts'] >= 3) {
            $this->cache->delete($cacheKey);
            return false;
        }
        
        // Timing-sicherer Vergleich
        $isValid = hash_equals($otpData['code'], $providedCode);
        
        if ($isValid) {
            // Erfolgreiche Verifikation - OTP löschen
            $this->cache->delete($cacheKey);
        } else {
            // Fehlversuch - Counter erhöhen
            $otpData['attempts']++;
            $this->cache->set($cacheKey, $otpData, 300);
        }
        
        return $isValid;
    }
}

Digitale Signaturen für Dokumente

Document Signing Service

final readonly class DocumentSigningService
{
    public function __construct(
        private DigitalSignature $signature,
        private DocumentRepository $documentRepository
    ) {}
    
    public function generateSigningKeyPair(): KeyPair
    {
        // RSA 4096 Bit für langfristige Dokumentensignaturen
        return $this->signature->generateRsaKeyPair(4096);
    }
    
    public function signDocument(
        Document $document, 
        PrivateKey $privateKey, 
        User $signer
    ): SignedDocument {
        $documentContent = $document->getContent();
        $metadata = [
            'document_id' => $document->getId(),
            'signer_id' => $signer->getId(),
            'signed_at' => (new DateTimeImmutable())->format('c'),
            'algorithm' => 'RSA-SHA256'
        ];
        
        // Dokument + Metadaten signieren
        $dataToSign = $documentContent . json_encode($metadata);
        $signatureResult = $this->signature->sign($dataToSign, $privateKey, 'sha256');
        
        $signedDocument = new SignedDocument(
            originalDocument: $document,
            signature: $signatureResult,
            signerPublicKey: $privateKey->getPublicKey(),
            metadata: $metadata,
            signedAt: new DateTimeImmutable()
        );
        
        return $this->documentRepository->saveSignedDocument($signedDocument);
    }
    
    public function verifyDocumentSignature(SignedDocument $signedDocument): bool
    {
        $document = $signedDocument->getOriginalDocument();
        $metadata = $signedDocument->getMetadata();
        
        $dataToVerify = $document->getContent() . json_encode($metadata);
        
        return $this->signature->verify(
            $dataToVerify,
            $signedDocument->getSignature(),
            $signedDocument->getSignerPublicKey()
        );
    }
    
    public function createSignatureChain(array $documents, array $signers): SignatureChain
    {
        $chain = new SignatureChain();
        
        foreach ($documents as $index => $document) {
            $signer = $signers[$index];
            $signedDocument = $this->signDocument($document, $signer->getPrivateKey(), $signer);
            $chain->addSignedDocument($signedDocument);
        }
        
        // Chain-Signatur erstellen (Hash der gesamten Kette)
        $chainHash = $chain->calculateChainHash();
        $chainSignature = $this->signature->sign($chainHash, $signers[0]->getPrivateKey());
        $chain->setChainSignature($chainSignature);
        
        return $chain;
    }
}

Passwort-Reset System

Secure Password Reset

final readonly class PasswordResetService
{
    public function __construct(
        private SecureTokenGenerator $tokenGenerator,
        private KeyDerivationFunction $kdf,
        private CacheInterface $cache,
        private EmailService $emailService
    ) {}
    
    public function initiatePasswordReset(User $user): void
    {
        // Sicheren Reset-Token generieren
        $resetToken = $this->tokenGenerator->generateVerificationToken('password_reset');
        
        // Token hashen und speichern (nie Klartext speichern)
        $tokenHash = hash('sha256', $resetToken->getValue());
        $resetData = [
            'user_id' => $user->getId(),
            'token_hash' => $tokenHash,
            'created_at' => time(),
            'used' => false
        ];
        
        // 2 Stunden gültig
        $cacheKey = "password_reset_{$user->getId()}";
        $this->cache->set($cacheKey, $resetData, 7200);
        
        // Reset-Link per Email senden
        $resetLink = "https://app.example.com/reset-password?token=" . 
                    urlencode($resetToken->getValue());
        
        $this->emailService->send(
            to: $user->getEmail(),
            subject: 'Password Reset Request',
            template: 'password_reset',
            data: ['reset_link' => $resetLink, 'expires_in' => '2 hours']
        );
    }
    
    public function validateResetToken(string $token): ?User
    {
        $tokenHash = hash('sha256', $token);
        
        // Token in allen aktiven Reset-Anfragen suchen
        $cacheKeys = $this->cache->getKeysByPattern('password_reset_*');
        
        foreach ($cacheKeys as $cacheKey) {
            $resetData = $this->cache->get($cacheKey);
            
            if ($resetData && 
                !$resetData['used'] && 
                hash_equals($resetData['token_hash'], $tokenHash)) {
                
                return $this->userRepository->find($resetData['user_id']);
            }
        }
        
        return null;
    }
    
    public function resetPassword(string $token, string $newPassword): bool
    {
        $user = $this->validateResetToken($token);
        
        if (!$user) {
            return false;
        }
        
        // Neues Password hashen
        $hashedPassword = $this->kdf->hashPassword($newPassword, 'argon2id');
        
        // User Password aktualisieren
        $user->updatePassword($hashedPassword);
        $this->userRepository->save($user);
        
        // Reset-Token als verwendet markieren
        $cacheKey = "password_reset_{$user->getId()}";
        $resetData = $this->cache->get($cacheKey);
        if ($resetData) {
            $resetData['used'] = true;
            $this->cache->set($cacheKey, $resetData, 7200);
        }
        
        return true;
    }
}

Daten-Verschlüsselung mit Schlüssel-Ableitung

Encrypted Data Storage

final readonly class EncryptedDataService
{
    public function __construct(
        private KeyDerivationFunction $kdf,
        private CryptographicUtilities $utils,
        private EncryptionService $encryption
    ) {}
    
    public function encryptPersonalData(
        array $personalData, 
        string $userPassword
    ): EncryptedPersonalData {
        // Benutzer-spezifischen Schlüssel aus Password ableiten
        $salt = $this->utils->generateNonce(32);
        $derivedKey = $this->kdf->pbkdf2($userPassword, $salt, 100000, 32);
        
        // Daten serialisieren und verschlüsseln
        $serializedData = json_encode($personalData);
        $encryptedData = $this->encryption->encrypt($serializedData, $derivedKey->getKey());
        
        return new EncryptedPersonalData(
            encryptedData: $encryptedData,
            salt: $salt,
            keyDerivation: [
                'algorithm' => $derivedKey->getAlgorithm(),
                'iterations' => $derivedKey->getIterations()
            ]
        );
    }
    
    public function decryptPersonalData(
        EncryptedPersonalData $encryptedData, 
        string $userPassword
    ): array {
        // Gleichen Schlüssel aus Password ableiten
        $derivedKey = $this->kdf->pbkdf2(
            $userPassword,
            $encryptedData->getSalt(),
            $encryptedData->getKeyDerivation()['iterations'],
            32
        );
        
        // Daten entschlüsseln
        $decryptedData = $this->encryption->decrypt(
            $encryptedData->getEncryptedData(),
            $derivedKey->getKey()
        );
        
        return json_decode($decryptedData, true);
    }
}

Audit-Trail mit Kryptographischen Fingerprints

Secure Audit Trail

final readonly class AuditTrailService
{
    public function __construct(
        private CryptographicUtilities $utils,
        private DigitalSignature $signature,
        private AuditLogRepository $repository
    ) {}
    
    public function logUserAction(
        User $user, 
        string $action, 
        array $context = []
    ): AuditLogEntry {
        $timestamp = new DateTimeImmutable();
        $logData = [
            'user_id' => $user->getId(),
            'action' => $action,
            'context' => $context,
            'timestamp' => $timestamp->format('c'),
            'ip_address' => $this->getClientIpHash(),
            'user_agent_hash' => $this->getUserAgentHash()
        ];
        
        // Kryptographischen Fingerprint erstellen
        $dataString = json_encode($logData, JSON_SORT_KEYS);
        $fingerprint = hash('sha256', $dataString);
        
        // Chain-Hash (verhindert Manipulation der Historie)
        $previousEntry = $this->repository->getLatestEntry();
        $chainHash = $previousEntry 
            ? hash('sha256', $previousEntry->getChainHash() . $fingerprint)
            : $fingerprint;
        
        $auditEntry = new AuditLogEntry(
            id: Ulid::generate(),
            fingerprint: $fingerprint,
            chainHash: $chainHash,
            logData: $logData,
            createdAt: $timestamp
        );
        
        return $this->repository->save($auditEntry);
    }
    
    public function verifyAuditTrailIntegrity(): bool
    {
        $entries = $this->repository->getAllEntriesOrdered();
        $previousChainHash = null;
        
        foreach ($entries as $entry) {
            // Fingerprint verifizieren
            $dataString = json_encode($entry->getLogData(), JSON_SORT_KEYS);
            $expectedFingerprint = hash('sha256', $dataString);
            
            if (!hash_equals($entry->getFingerprint(), $expectedFingerprint)) {
                return false; // Entry wurde manipuliert
            }
            
            // Chain-Hash verifizieren
            $expectedChainHash = $previousChainHash 
                ? hash('sha256', $previousChainHash . $entry->getFingerprint())
                : $entry->getFingerprint();
            
            if (!hash_equals($entry->getChainHash(), $expectedChainHash)) {
                return false; // Chain wurde unterbrochen
            }
            
            $previousChainHash = $entry->getChainHash();
        }
        
        return true;
    }
    
    private function getClientIpHash(): string
    {
        $clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        return hash('sha256', $clientIp); // IP nicht im Klartext speichern
    }
    
    private function getUserAgentHash(): string
    {
        $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
        return hash('sha256', $userAgent);
    }
}

Diese Beispiele zeigen die praktische Anwendung des Cryptography Modules in realen Szenarien und demonstrieren Best Practices für sichere Implementierungen.