privateSeed(); $pair = sodium_crypto_sign_seed_keypair($seed); $secret = sodium_crypto_sign_secretkey($pair); $payload = [ 'customerEmail' => $license->customer_email, 'customerName' => $license->customer_name, 'edition' => $license->edition, 'expiresAt' => $license->expires_at?->utc()->format('Y-m-d\\TH:i:s\\Z'), 'installationID' => strtolower($installationID), 'issuedAt' => ($issuedAt ?? now())->utc()->format('Y-m-d\\TH:i:s\\Z'), 'licenseID' => strtolower((string) $license->id), 'maxMajorVersion' => $license->max_major_version, ]; ksort($payload, SORT_STRING); $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); $sig = sodium_crypto_sign_detached($json, $secret); return 'RS3.'.self::b64url($json).'.'.self::b64url($sig); } public function publicKeyBase64(): string { $pair = sodium_crypto_sign_seed_keypair($this->privateSeed()); return base64_encode(sodium_crypto_sign_publickey($pair)); } private function privateSeed(): string { $path = config('rsync-studio.private_key_path'); if (! $path || ! is_file($path)) { throw new RuntimeException('RSYNC_STUDIO_LICENSE_PRIVATE_KEY_PATH is not configured or does not exist.'); } $decoded = base64_decode(trim((string) file_get_contents($path)), true); if ($decoded === false || strlen($decoded) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) { throw new RuntimeException('The Rsync Studio private key must be a Base64-encoded 32-byte Ed25519 seed.'); } return $decoded; } private static function b64url(string $value): string { return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); } }