UNPKG

factom-vote

Version:

JS implementation of the Factom voting specification

163 lines (134 loc) 5.1 kB
const base58 = require('base-58'), sign = require('tweetnacl/nacl-fast').sign, { getKeyPair, sha256d } = require('./crypto'); const ID_PUB_PREFIX = Buffer.from('0345ef9de0', 'hex'), ID_SEC_PREFIX = Buffer.from('0345f3d0d6', 'hex'); const VALID_ID_PREFIXES = new Set(['idpub', 'idsec']); async function getVoteIdentity(identityResolvers, identity) { const publicIdentityKey = getPublicIdentityKey(identity.key); await verifyIdentityKeyAssociation(identityResolvers.publicKeysResolver, identity.chainId, publicIdentityKey); const publicKey = extractKey(publicIdentityKey); const result = { id: identity.chainId, publicKey }; if (typeof identity.sign === 'function') { result.sign = identity.sign; } else { result.secretKey = extractKey(await getSecretIdentityKey(identityResolvers.privateKeyResolver, identity.key)); } return result; } async function getSignature(voteIdentity, dataToSign) { if (typeof voteIdentity.sign === 'function') { const signature = Buffer.from(await voteIdentity.sign(dataToSign), 'hex'); if (!sign.detached.verify(dataToSign, signature, voteIdentity.publicKey)) { throw new Error(`Identity signature does not verify for public key ${voteIdentity.publicKey.toString('hex')}`); } return signature; } else if (voteIdentity.secretKey) { const keyPair = getKeyPair(voteIdentity.secretKey); return sign.detached(dataToSign, keyPair.secretKey); } else { throw new Error('Unable to sign data: no resolvable secret key or sign() method'); } } async function verifyIdentityKeyAssociation(identityPublicKeysResolver, identityChainId, identityKey, blockHeight) { const publicIdentityKey = getPublicIdentityKey(identityKey); const keys = await identityPublicKeysResolver.call(null, identityChainId, blockHeight); if (!keys.includes(publicIdentityKey)) { throw new Error(`Public identity key [${publicIdentityKey}] is not associated with identity [${identityChainId}] at block height ${blockHeight}.`); } } function extractKey(identityKey) { if (!isValidIdentityKey(identityKey)) { throw new Error(`Invalid identity key ${identityKey}.`); } return Buffer.from(base58.decode(identityKey).slice(5, 37)); } function getPublicIdentityKey(key) { if (isValidPublicIdentityKey(key)) { return key; } else if (isValidSecretIdentityKey(key)) { const publicKey = getKeyPair(extractKey(key)).publicKey; return keyToPublicIdentityKey(publicKey); } else { throw new Error(`Invalid identity key: ${key}`); } } function getSecretIdentityKey(privateKeyResolver, key) { if (isValidSecretIdentityKey(key)) { return key; } else if (isValidPublicIdentityKey(key)) { return privateKeyResolver.call(null, key); } else { throw new Error(`Invalid identity key: ${key}`); } } function isValidIdentityKey(key) { try { if (!VALID_ID_PREFIXES.has(key.slice(0, 5))) { return false; } const bytes = Buffer.from(base58.decode(key)); if (bytes.length !== 41) { return false; } const checksum = sha256d(bytes.slice(0, 37)).slice(0, 4); if (checksum.equals(bytes.slice(37, 41))) { return true; } return false; } catch (err) { return false; } } function isValidPublicIdentityKey(key) { return isValidIdentityKey(key) && key.startsWith('idpub'); } function isValidSecretIdentityKey(key) { return isValidIdentityKey(key) && key.startsWith('idsec'); } function keyToIdentityKey(key, prefix) { const keyBuffer = Buffer.from(key, 'hex'); if (keyBuffer.length !== 32) { throw new Error(`Key ${keyBuffer} is not 32 bytes long.`); } const address = Buffer.concat([prefix, keyBuffer]); const checksum = sha256d(address).slice(0, 4); return base58.encode(Buffer.concat([address, checksum])); } function keyToPublicIdentityKey(key) { return keyToIdentityKey(key, ID_PUB_PREFIX); } function keyToSecretIdentityKey(key) { return keyToIdentityKey(key, ID_SEC_PREFIX); } /////////////////////////////// // Walletd identity resolvers /////////////////////////////// async function walletdIdentityPublicKeysResolver(cli, identityChainId, blockHeight) { const { keys } = await cli.walletdApi('active-identity-keys', { chainid: identityChainId, height: blockHeight }); return keys; } async function walletdIdentityPrivateKeyResolver(cli, key) { return cli.walletdApi('identity-key', { public: key }).then(data => data.secret); } module.exports = { extractKey, getVoteIdentity, getPublicIdentityKey, getSecretIdentityKey, isValidIdentityKey, isValidPublicIdentityKey, isValidSecretIdentityKey, keyToPublicIdentityKey, keyToSecretIdentityKey, verifyIdentityKeyAssociation, walletdIdentityPublicKeysResolver, walletdIdentityPrivateKeyResolver, getSignature };