137 lines
4.2 KiB
PHP
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';
|
|
}
|
|
}
|