Files
kami-parse-server/src/Adapters/Auth/gcenter.js

137 lines
3.9 KiB
JavaScript

/* 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,
};