mailwolt/app/Services/DkimService.php

141 lines
5.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services;
use App\Models\Domain;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
class DkimService
{
/** Erzeugt Keypair & gibt den TXT-Record (ohne Host) zurück. */
public function generateForDomain(Domain $domainId, int $bits = 2048, string $selector = 'dkim'): array
{
$dirKey = $this->safeKey($domainId);
$selKey = $this->safeKey($selector, 32);
$disk = Storage::disk('local');
$baseRel = "dkim/{$dirKey}";
$privRel = "{$baseRel}/{$selKey}.pem";
$pubRel = "{$baseRel}/{$selKey}.pub";
// 1) Ordner sicherstellen
$disk->makeDirectory($baseRel);
// 2) Keypair via PHP-OpenSSL (kein shell_exec)
$res = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'private_key_bits' => $bits,
]);
if ($res === false) {
throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
}
$privateKey = '';
if (!openssl_pkey_export($res, $privateKey)) {
throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
}
$details = openssl_pkey_get_details($res);
if ($details === false || empty($details['key'])) {
throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.');
}
$publicKeyPem = $details['key'];
Log::debug('dkim.pem.first_line', ['line' => strtok($publicKeyPem, "\n")]);
Log::debug('dkim.pem.len', ['len' => strlen($publicKeyPem)]);
// 3) Schreiben über Storage (legt Dateien an, keine zu langen Pfade in Komponenten)
if (!$disk->put($privRel, $privateKey)) {
throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privRel}");
}
if (!$disk->put($pubRel, $publicKeyPem)) {
throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubRel}");
}
// 4) DNS-Record bauen
// $p = preg_replace('/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s+/', '', $publicKeyPem);
// $dnsTxt = "v=DKIM1; k=rsa; p={$p}";
$publicKeyBase64 = self::extractPublicKeyBase64($publicKeyPem);
Log::debug('dkim.p.len', ['len' => strlen($publicKeyBase64)]);
$dnsTxt = "v=DKIM1; k=rsa; p={$publicKeyBase64}";
// sanity: RSA2048 liegt typ. > 300 chars
if (strlen($publicKeyBase64) < 300) {
throw new \RuntimeException('DKIM: Public Key zu kurz vermutlich Parsing-Fehler.');
}
return [
'selector' => $selKey,
'priv_path' => storage_path("app/{$privRel}"),
'pub_path' => storage_path("app/{$pubRel}"),
'public_pem' => $publicKeyPem,
'private_pem' => $privateKey,
'dns_name' => "{$selKey}._domainkey", // vor Domain hängen
'dns_txt' => $dnsTxt,
'bits' => $bits,
];
}
// // Pfade
// $base = "dkim/{$domain->id}";
// Storage::disk('local')->makeDirectory($base);
//
// $privPath = storage_path("app/{$base}/{$selector}.key");
// $pubPath = storage_path("app/{$base}/{$selector}.pub");
//
// // openssl genrsa / rsa-privkey
// $cmd = sprintf('openssl genrsa %d > %s && openssl rsa -in %s -pubout -out %s',
// $bits, escapeshellarg($privPath), escapeshellarg($privPath), escapeshellarg($pubPath)
// );
// shell_exec($cmd);
//
// $pub = trim(file_get_contents($pubPath));
// // Public Key extrahieren → DKIM TXT
// $pub = preg_replace('/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s+/', '', $pub);
//
// $txt = "v=DKIM1; k=rsa; p={$pub}";
// // Domain kann hier auch den Pfad/Selector speichern:
// $domain->update([
// 'dkim_selector' => $selector,
// 'dkim_bits' => $bits,
// 'dkim_key_path' => $privPath,
// ]);
//
// return $txt;
// }
protected function safeKey($value, int $max = 64): string
{
if (is_object($value)) {
if (isset($value->id)) $value = $value->id;
elseif (method_exists($value, 'getKey')) $value = $value->getKey();
else $value = json_encode($value);
}
$raw = (string) $value;
$san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw);
if ($san === '' ) $san = 'unknown';
if (strlen($san) > $max) {
$san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12);
}
return $san;
}
protected static function extractPublicKeyBase64(string $pem): string
{
// Hole den Body zwischen den Headern (multiline, dotall)
if (!preg_match('/^-+BEGIN PUBLIC KEY-+\r?\n(.+?)\r?\n-+END PUBLIC KEY-+\s*$/ms', $pem, $m)) {
throw new \RuntimeException('DKIM: Ungültiges Public-Key-PEM (Header/Footers nicht gefunden).');
}
// Whitespace entfernen → reines Base64
$b64 = preg_replace('/\s+/', '', $m[1]);
if ($b64 === '' || base64_decode($b64, true) === false) {
throw new \RuntimeException('DKIM: Public Key Base64 ist leer/ungültig.');
}
return $b64;
}
}