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

70 lines (69 loc) 2.98 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports._cache = void 0; exports.convertX509CertToPEM = convertX509CertToPEM; exports.getHttpCached = getHttpCached; exports.convertTimestampToBigEndian = convertTimestampToBigEndian; exports.verifyIdToken = verifyIdToken; exports.assertValid = assertValid; exports.verify = verify; const node_https_1 = require("node:https"); const node_crypto_1 = require("node:crypto"); const node_assert_1 = require("node:assert"); exports._cache = {}; // (publicKey -> cert) cache 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; } async function getHttpCached(url) { if (exports._cache[url] && exports._cache[url].expires > Date.now()) return exports._cache[url].data; return new Promise((resolve, reject) => (0, node_https_1.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', () => { exports._cache[url] = cacheEntry; resolve(cacheEntry.data); }); })); } 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; } function verifyIdToken(publicKeyPEM, idToken) { const verifier = (0, node_crypto_1.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'); } function assertValid(input) { (0, node_assert_1.ok)(input.bundleId && input.playerId && input.publicKey && input.salt && input.timestamp && input.signature, 'falsy data found'); const url = new URL(input.publicKey); (0, node_assert_1.ok)(url.protocol == 'https:' && url.host.endsWith('.apple.com')); return input; } async function verify(idToken) { const x509Cert = await getHttpCached(idToken.publicKey); const pem = convertX509CertToPEM(x509Cert); return verifyIdToken(pem, idToken); }