UNPKG

gamecenter-identity-verifier-js

Version:

A small library to verify the identity of apple-gamecenter's local player for consuming it in node.js backend server. A rewrite to typescript of maeltm/node-gamecenter-identity-verifier

61 lines (60 loc) 2.53 kB
import { get } from 'node:https'; import { createVerify } from 'node:crypto'; import { ok } from 'node:assert'; export const _cache = {}; // (publicKey -> cert) cache export function convertX509CertToPEM(X509Cert) { const pemPreFix = '-----BEGIN CERTIFICATE-----\n'; const pemPostFix = '-----END CERTIFICATE-----'; const base64 = X509Cert; const certBody = base64.match(/.{0,64}/g)?.join('\n'); return pemPreFix + certBody + pemPostFix; } export async function getHttpCached(url) { if (_cache[url] && _cache[url].expires > Date.now()) return _cache[url].data; return new Promise((resolve, reject) => get(url, (res) => { if (res.statusCode !== 200) { return reject(new Error(`getHttpCached: ${url} responded ${res.statusCode}, expected 200.`)); } const httpMaxAgeMs = (parseInt(res.headers['cache-control']?.match(/max-age=([0-9]+)/)?.[1]) * 1000) || 0; const cacheEntry = { data: '', expires: Date.now() + httpMaxAgeMs }; res.on('error', reject) .on('data', chunk => cacheEntry.data += chunk.toString('base64')) .on('end', () => { _cache[url] = cacheEntry; resolve(cacheEntry.data); }); })); } export function convertTimestampToBigEndian(timestamp) { // The timestamp parameter in Big-Endian UInt-64 format const buffer = Buffer.alloc(8); buffer.fill(0); const high = ~~(timestamp / 0xffffffff); const low = timestamp % (0xffffffff + 0x1); buffer.writeUInt32BE(parseInt(high, 10), 0); buffer.writeUInt32BE(parseInt(low, 10), 4); return buffer; } export function verifyIdToken(publicKeyPEM, idToken) { const verifier = createVerify('sha256'); verifier.update(idToken.playerId, 'utf8'); verifier.update(idToken.bundleId, 'utf8'); verifier.update(convertTimestampToBigEndian(idToken.timestamp)); verifier.update(idToken.salt, 'base64'); return verifier.verify(publicKeyPEM, idToken.signature, 'base64'); } export function assertValid(input) { ok(input.bundleId && input.playerId && input.publicKey && input.salt && input.timestamp && input.signature, 'falsy data found'); const url = new URL(input.publicKey); ok(url.protocol == 'https:' && url.host.endsWith('.apple.com')); return input; } export async function verify(idToken) { const x509Cert = await getHttpCached(idToken.publicKey); const pem = convertX509CertToPEM(x509Cert); return verifyIdToken(pem, idToken); }