apple-gamecenter-identity-verifier
Version:
A small library to verify the identity of apple-gamecenter's local player for consuming it in node.js backend server.
105 lines (104 loc) • 3.52 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.verify = void 0;
const crypto_1 = require("crypto");
const url = require("url");
const https = require("https");
const cache = {}; // (publicKey -> cert) cache
function verify(idToken, callback) {
getAppleCertificate(idToken.publicKeyUrl, (err, publicKey) => {
if (!err) {
try {
verifySignature(publicKey, idToken);
callback(null, idToken);
}
catch (e) {
callback(e, null);
}
}
else {
callback(err, null);
}
});
}
exports.verify = verify;
function verifyPublicKeyUrl(publicKeyUrl) {
const parsedUrl = url.parse(publicKeyUrl);
if (parsedUrl.protocol !== 'https:') {
return false;
}
if (parsedUrl.hostname == null) {
return false;
}
const hostnameParts = parsedUrl.hostname.split('.');
const length = hostnameParts.length;
const domainParts = hostnameParts.slice(length - 2, length);
const domain = domainParts.join('.');
if (domain !== 'apple.com') {
return false;
}
return true;
}
function getAppleCertificate(publicKeyUrl, callback) {
if (!verifyPublicKeyUrl(publicKeyUrl)) {
callback(new Error('Invalid publicKeyUrl'), null);
return;
}
if (cache[publicKeyUrl]) {
return callback(null, cache[publicKeyUrl]);
}
https
.get(publicKeyUrl, (res) => {
var data = '';
res.on('data', (chunk) => {
data += chunk.toString('base64');
});
res.on('end', () => {
var cert = convertX509CertToPEM(data);
if (res.headers['cache-control']) {
// if there's a cache-control header
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
if (expire) {
// if we got max-age
cache[publicKeyUrl] = cert; // save in cache
// we'll expire the cache entry later, as per max-age
setTimeout(() => {
delete cache[publicKeyUrl];
}, parseInt(expire[1], 10) * 1000);
}
}
callback(null, cert);
});
})
.on('error', (e) => {
callback(e);
});
}
function convertX509CertToPEM(X509Cert) {
var _a;
var pemPreFix = '-----BEGIN CERTIFICATE-----\n';
var pemPostFix = '-----END CERTIFICATE-----';
var base64 = X509Cert;
var certBody = (_a = base64.match(new RegExp('.{0,64}', 'g'))) === null || _a === void 0 ? void 0 : _a.join('\n');
return pemPreFix + certBody + pemPostFix;
}
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(high, 0);
buffer.writeUInt32BE(low, 4);
return buffer;
}
function verifySignature(publicKey, idToken) {
var verifier = (0, crypto_1.createVerify)('sha256');
verifier.update(idToken.playerId, 'utf8');
verifier.update(idToken.bundleId, 'utf8');
verifier.update(convertTimestampToBigEndian(idToken.timestamp));
verifier.update(idToken.salt, 'base64');
if (!verifier.verify(publicKey, idToken.signature, 'base64')) {
throw new Error('Invalid Signature');
}
}