141 lines
5.3 KiB
PHP
141 lines
5.3 KiB
PHP
<?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;
|
||
}
|
||
}
|