api/app/Services/AdMobServerSideVerification...

137 lines
4.2 KiB
PHP

<?php
namespace App\Services;
use App\Exceptions\InvalidRewardedAdCallback;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class AdMobServerSideVerificationVerifier
{
public function verify(Request $request): void
{
$queryString = (string) $request->server('QUERY_STRING', '');
$signatureParameter = 'signature=';
$keyIdParameter = 'key_id=';
$signatureStart = strpos($queryString, $signatureParameter);
if ($queryString === '' || $signatureStart === false || $signatureStart === 0) {
throw new InvalidRewardedAdCallback('Missing rewarded ad signature.');
}
$content = substr($queryString, 0, $signatureStart - 1);
$signatureAndKey = substr($queryString, $signatureStart);
$keyIdStart = strpos($signatureAndKey, $keyIdParameter);
if ($keyIdStart === false) {
throw new InvalidRewardedAdCallback('Missing rewarded ad key id.');
}
$encodedSignature = substr(
$signatureAndKey,
strlen($signatureParameter),
$keyIdStart - strlen($signatureParameter) - 1,
);
$keyId = strtok(substr($signatureAndKey, $keyIdStart + strlen($keyIdParameter)), '&');
if ($encodedSignature === '' || $keyId === false || $keyId === '') {
throw new InvalidRewardedAdCallback('Invalid rewarded ad signature parameters.');
}
$signature = $this->base64UrlDecode(rawurldecode($encodedSignature));
$publicKey = $this->publicKey((string) $keyId);
if (openssl_verify($content, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
throw new InvalidRewardedAdCallback('Invalid rewarded ad signature.');
}
}
private function publicKey(string $keyId): string
{
$publicKeys = $this->publicKeys();
if (! isset($publicKeys[$keyId])) {
Cache::forget($this->cacheKey());
$publicKeys = $this->publicKeys();
}
if (! isset($publicKeys[$keyId])) {
throw new InvalidRewardedAdCallback('Unknown rewarded ad signature key.');
}
return $publicKeys[$keyId];
}
/**
* @return array<string, string>
*/
private function publicKeys(): array
{
return Cache::remember(
$this->cacheKey(),
(int) config('admob.rewarded.public_keys_cache_seconds', 43200),
function (): array {
$response = Http::timeout(5)
->acceptJson()
->get((string) config('admob.rewarded.public_keys_url'));
if (! $response->ok()) {
throw new InvalidRewardedAdCallback('Unable to fetch rewarded ad public keys.');
}
$keys = $response->json('keys');
if (! is_array($keys)) {
throw new InvalidRewardedAdCallback('Invalid rewarded ad public keys.');
}
$publicKeys = [];
foreach ($keys as $key) {
if (! is_array($key) || ! isset($key['keyId'])) {
continue;
}
$pem = $key['pem'] ?? null;
if (! is_string($pem) || $pem === '') {
continue;
}
$publicKeys[(string) $key['keyId']] = $pem;
}
if ($publicKeys === []) {
throw new InvalidRewardedAdCallback('No rewarded ad public keys are available.');
}
return $publicKeys;
}
);
}
private function base64UrlDecode(string $value): string
{
$normalized = strtr($value, '-_', '+/');
$padding = strlen($normalized) % 4;
if ($padding > 0) {
$normalized .= str_repeat('=', 4 - $padding);
}
$decoded = base64_decode($normalized, true);
if ($decoded === false) {
throw new InvalidRewardedAdCallback('Invalid rewarded ad signature encoding.');
}
return $decoded;
}
private function cacheKey(): string
{
return 'admob:ssv-public-keys';
}
}