/* Apple Game Center Auth https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign#discussion const authData = { publicKeyUrl: 'https://valid.apple.com/public/timeout.cer', timestamp: 1460981421303, signature: 'PoDwf39DCN464B49jJCU0d9Y0J', salt: 'saltST==', bundleId: 'com.valid.app' id: 'playerId', }; */ const { Parse } = require('parse/node'); const crypto = require('crypto'); const https = require('https'); const cache = {}; // (publicKey -> cert) cache function verifyPublicKeyUrl(publicKeyUrl) { try { const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/; return regex.test(publicKeyUrl); } catch (error) { return false; } } function convertX509CertToPEM(X509Cert) { const pemPreFix = '-----BEGIN CERTIFICATE-----\n'; const pemPostFix = '-----END CERTIFICATE-----'; const base64 = X509Cert; const certBody = base64.match(new RegExp('.{0,64}', 'g')).join('\n'); return pemPreFix + certBody + pemPostFix; } async function getAppleCertificate(publicKeyUrl) { if (!verifyPublicKeyUrl(publicKeyUrl)) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}` ); } if (cache[publicKeyUrl]) { return cache[publicKeyUrl]; } const url = new URL(publicKeyUrl); const headOptions = { hostname: url.hostname, path: url.pathname, method: 'HEAD', }; const headers = await new Promise((resolve, reject) => https.get(headOptions, res => resolve(res.headers)).on('error', reject) ); if ( headers['content-type'] !== 'application/pkix-cert' || headers['content-length'] == null || headers['content-length'] > 10000 ) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}` ); } return new Promise((resolve, reject) => { https .get(publicKeyUrl, res => { let data = ''; res.on('data', chunk => { data += chunk.toString('base64'); }); res.on('end', () => { const cert = convertX509CertToPEM(data); if (res.headers['cache-control']) { var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/); if (expire) { cache[publicKeyUrl] = cert; // we'll expire the cache entry later, as per max-age setTimeout(() => { delete cache[publicKeyUrl]; }, parseInt(expire[1], 10) * 1000); } } resolve(cert); }); }) .on('error', reject); }); } function convertTimestampToBigEndian(timestamp) { const buffer = Buffer.alloc(8); const high = ~~(timestamp / 0xffffffff); const low = timestamp % (0xffffffff + 0x1); buffer.writeUInt32BE(parseInt(high, 10), 0); buffer.writeUInt32BE(parseInt(low, 10), 4); return buffer; } function verifySignature(publicKey, authData) { const verifier = crypto.createVerify('sha256'); verifier.update(authData.playerId, 'utf8'); verifier.update(authData.bundleId, 'utf8'); verifier.update(convertTimestampToBigEndian(authData.timestamp)); verifier.update(authData.salt, 'base64'); if (!verifier.verify(publicKey, authData.signature, 'base64')) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - invalid signature'); } } // Returns a promise that fulfills if this user id is valid. async function validateAuthData(authData) { if (!authData.id) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - authData id missing'); } authData.playerId = authData.id; const publicKey = await getAppleCertificate(authData.publicKeyUrl); return verifySignature(publicKey, authData); } // Returns a promise that fulfills if this app id is valid. function validateAppId() { return Promise.resolve(); } module.exports = { validateAppId, validateAuthData, };