bitcore-wallet-client
Version:
Client for bitcore-wallet-service
276 lines (252 loc) • 8.55 kB
text/typescript
const ncrypto = require('crypto'); // renamed in order to prevent redeclaration of block-scoped variable 'crypto' from typescript dom library
const bs58 = require('bs58');
const kbpgp = require('kbpgp');
const request = require('request-promise');
let bitpayPgpKeys = {};
let githubPgpKeys = {};
let importedPgpKeys = {};
let signatureCount = 0;
let eccPayload;
let parsedEccPayload;
let eccKeysHash;
let keyRequests = [];
keyRequests.push(
(() => {
console.log('Fetching keys from github.com/bitpay/pgp-keys...');
return request({
method: 'GET',
url: 'https://api.github.com/repos/bitpay/pgp-keys/contents/keys',
headers: {
'user-agent': 'BitPay Key-Check Utility'
},
json: true
}).then(pgpKeyFiles => {
let fileDataPromises = [];
pgpKeyFiles.forEach(file => {
fileDataPromises.push(
(() => {
return request({
method: 'GET',
url: file.download_url,
headers: {
'user-agent': 'BitPay Key-Check Utility'
}
}).then(body => {
let hash = ncrypto.createHash('sha256').update(body).digest('hex');
githubPgpKeys[hash] = body;
return Promise.resolve();
});
})()
);
});
return Promise.all(fileDataPromises);
});
})()
);
keyRequests.push(
(() => {
console.log('Fetching keys from bitpay.com/pgp-keys...');
return request({
method: 'GET',
url: 'https://bitpay.com/pgp-keys.json',
headers: {
'user-agent': 'BitPay Key-Check Utility'
},
json: true
}).then(body => {
body.pgpKeys.forEach(function (key) {
let hash = ncrypto
.createHash('sha256')
.update(key.publicKey)
.digest('hex');
bitpayPgpKeys[hash] = key.publicKey;
});
return Promise.resolve();
});
})()
);
Promise.all(keyRequests)
.then(() => {
if (
Object.keys(githubPgpKeys).length !== Object.keys(bitpayPgpKeys).length
) {
console.log('Warning: Different number of keys returned by key lists');
}
let bitpayOnlyKeys = Object.keys(bitpayPgpKeys).filter(keyHash => {
return !githubPgpKeys[keyHash];
});
let githubOnlyKeys = Object.keys(githubPgpKeys).filter(keyHash => {
return !bitpayPgpKeys[keyHash];
});
if (bitpayOnlyKeys.length) {
console.log('BitPay returned some keys which are not present in github');
Object.keys(bitpayOnlyKeys).forEach(keyHash => {
console.log(`Hash ${keyHash} Key: ${bitpayOnlyKeys[keyHash]}`);
});
}
if (githubOnlyKeys.length) {
console.log('GitHub returned some keys which are not present in BitPay');
Object.keys(githubOnlyKeys).forEach(keyHash => {
console.log(`Hash ${keyHash} Key: ${githubOnlyKeys[keyHash]}`);
});
}
if (!githubOnlyKeys.length && !bitpayOnlyKeys.length) {
console.log(
`Both sites returned ${
Object.keys(githubPgpKeys).length
} keys. Key lists from both are identical.`
);
return Promise.resolve();
} else {
return Promise.reject('Aborting signature checks due to key mismatch');
}
})
.then(() => {
console.log('Importing PGP keys for later use...');
return Promise.all(
Object.values(bitpayPgpKeys).map(pgpKeyString => {
return new Promise<void>((resolve, reject) => {
kbpgp.KeyManager.import_from_armored_pgp(
{ armored: pgpKeyString },
(err, km) => {
if (err) {
return reject(err);
}
importedPgpKeys[
km.pgp.key(km.pgp.primary).get_fingerprint().toString('hex')
] = km;
return resolve();
}
);
});
})
);
})
.then(() => {
console.log(
'Fetching current ECC keys from bitpay.com/signingKeys/paymentProtocol.json'
);
return request({
method: 'GET',
url: 'https://bitpay.com/signingKeys/paymentProtocol.json',
headers: {
'user-agent': 'BitPay Key-Check Utility'
}
}).then(rawEccPayload => {
if (rawEccPayload.indexOf('rate limit') !== -1) {
return Promise.reject('Rate limited by BitPay');
}
eccPayload = rawEccPayload;
parsedEccPayload = JSON.parse(rawEccPayload);
// if (new Date(parsedEccPayload.expirationDate) < Date.now()) {
// return console.log('The currently published ECC keys are expired');
// }
eccKeysHash = ncrypto
.createHash('sha256')
.update(rawEccPayload)
.digest('hex');
return Promise.resolve();
});
})
.then(() => {
console.log(`Fetching signatures for ECC payload with hash ${eccKeysHash}`);
return request({
method: 'GET',
url: `https://bitpay.com/signatures/${eccKeysHash}.json`,
headers: {
'user-agent': 'BitPay Key-Check Utility'
},
json: true
}).then(signatureData => {
console.log(
'Verifying each signature is valid and comes from the set of PGP keys retrieved earlier'
);
Promise.all(
signatureData.signatures.map(signature => {
return new Promise((resolve, reject) => {
let pgpKey = importedPgpKeys[signature.identifier];
if (!pgpKey) {
return reject(
`PGP key ${signature.identifier} missing for signature`
);
}
let armoredSignature = Buffer.from(
signature.signature,
'hex'
).toString();
kbpgp.unbox(
{
armored: armoredSignature,
data: Buffer.from(eccPayload),
keyfetch: pgpKey
},
(err, result) => {
if (err) {
return reject(
`Unable to verify signature from ${signature.identifier} ${err}`
);
}
signatureCount++;
console.log(
`Good signature from ${signature.identifier} (${pgpKey
.get_userids()[0]
.get_username()})`
);
return Promise.resolve();
}
);
});
})
);
});
})
.then(() => {
if (signatureCount >= Object.keys(bitpayPgpKeys).length / 2) {
console.log(
`----\nThe following ECC key set has been verified against signatures from ${signatureCount} of the ${
Object.keys(bitpayPgpKeys).length
} published BitPay PGP keys.`
);
console.log(eccPayload);
let keyMap = {};
console.log('----\nValid keymap for use in bitcoinRpc example:');
parsedEccPayload.publicKeys.forEach(pubkey => {
// Here we are just generating the pubkey hash (btc address) of each of the public keys received for easy lookup later
// as this is what will be provided by the x-identity header
let a = ncrypto.createHash('sha256').update(pubkey, 'hex').digest();
let b = ncrypto.createHash('rmd160').update(a).digest('hex');
let c = '00' + b; // This is assuming livenet
let d = ncrypto.createHash('sha256').update(c, 'hex').digest();
let e = ncrypto.createHash('sha256').update(d).digest('hex');
let pubKeyHash = bs58.encode(Buffer.from(c + e.substr(0, 8), 'hex'));
keyMap[pubKeyHash] = {
owner: parsedEccPayload.owner,
networks: ['main'],
domains: parsedEccPayload.domains,
publicKey: pubkey
};
// Add Bitpay's testnet
keyMap['mh65MN7drqmwpCRZcEeBEE9ceQCQ95HtZc'] = {
owner: 'BitPay (TESTNET ONLY - DO NOT TRUST FOR ACTUAL BITCOIN)',
networks: ['test'],
domains: ['test.bitpay.com'],
publicKey:
'03159069584176096f1c89763488b94dbc8d5e1fa7bf91f50b42f4befe4e45295a'
};
});
console.log(keyMap);
const fs = require('fs');
fs.writeFileSync(
'JsonPaymentProtocolKeys.js',
'module.exports = ' + JSON.stringify(keyMap, null, 2)
);
} else {
return Promise.reject(
`Insufficient good signatures ${signatureCount} for a proper validity check`
);
}
})
.catch(err => {
console.log(`Error encountered ${err}`);
});
process.on('unhandledRejection', console.log);