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
JavaScript
;
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);
}