/**
* @fileOverview Utility encryption related functions for KeyManager module.
*
* @author Raul Aragonez (r.aragonez@topl.me)
*
* @exports utils isValidNetwork, getUrlByNetwork, getHexByNetwork, getDecimalByNetwork, getValidNetworksList, validateAddressesByNetwork, generatePubKeyHashAddress
*/
"use strict";
// Dependencies
const Base58 = require("bs58");
const blake = require("blake2");
const validNetworks = ["local", "private", "toplnet", "valhalla", "hel"];
// TODO: Feature - support custom define network
// TODO: everything in UTILS should be part of BramblJS
const networksDefaults = {
"local": {
hex: "0x30",
decimal: 48
},
"private": {
hex: "0x40",
decimal: 64
},
"toplnet": {
hex: "0x01",
decimal: 1
},
"valhalla": {
hex: "0x10",
decimal: 16
},
"hel": {
hex: "0x20",
decimal: 32
}
};
/**
* Check if addresses are valid by verifying:
* 1. verify the address is not null
* 2. verify the base58 is 38 bytes long
* 3. verify that it matches the network
* 4. verify that hash matches the last 4 bytes
* @param {String} networkPrefix prefix of network to validate against
* @param {Array} addresses list of addresses to run validation against
* @returns {object} result obj returned as json
*/
function validateAddressesByNetwork(networkPrefix, addresses) {
// response upon the completion of validation
const result = {
success: false,
errorMsg: "",
networkPrefix: networkPrefix,
addresses: [],
invalidAddresses: [],
invalidChecksums: []
};
// check if network is valid
if (!isValidNetwork(networkPrefix)) {
result.errorMsg = "Invalid network provided";
return result;
}
if (!addresses) {
result.errorMsg = "No addresses provided";
return result;
}
// get decimal of the network prefix
const networkDecimal = getDecimalByNetwork(networkPrefix);
// addresses can be passed as an array or extracted from a json obj
result.addresses = addresses.constructor === Array ? addresses : extractAddressesFromObj(addresses);
// check if addresses were obtained
if (!result.addresses || result.addresses.length < 1) {
result.errorMsg = "No addresses found";
return result;
}
// run validation on addresses, if address is not valid then add it to invalidAddresses array
result.addresses.forEach((address) => {
const decodedAddress = Base58.decode(address);
// validation: base58 38 byte obj that matches networkPrefix decimal
if (decodedAddress.length !== 38 || decodedAddress[0] !== networkDecimal) {
result.invalidAddresses.push(address);
} else {
// address has correct length and matches the network, now validate the checksum
const checksumBuffer = Buffer.from(decodedAddress.slice(34));
// encrypt message (bytes 1-34)
const msgBuffer = Buffer.from(decodedAddress.slice(0, 34));
const hashChecksumBuffer = blake.createHash("blake2b", {digestLength: 32}).update(msgBuffer).end().read().slice(0, 4);
// verify checksum bytes match
if (!checksumBuffer.equals(hashChecksumBuffer)) {
result.invalidChecksums.push(address);
}
}
});
// check if any invalid addresses were found
if (result.invalidAddresses.length > 0) {
result.errorMsg = "Invalid addresses for network: " + networkPrefix;
} else if (result.invalidChecksums.length > 0) {
result.errorMsg = "Addresses with invalid checksums found.";
} else {
result.success = true;
}
return result;
}
/**
* Generate Hash Address using the Public Key and Network Prefix
* @param {Buffer} publicKey base58 buffer of public key
* @param {String} networkPrefix prefix of network where address will be used
* @returns {object} result obj returned as json
*/
function generatePubKeyHashAddress(publicKey, networkPrefix) {
const result = {
success: false,
errorMsg: "",
networkPrefix: networkPrefix,
address: ""
};
// validate Network Prefix
if (!isValidNetwork(networkPrefix)) {
result.errorMsg = "Invalid network provided";
return result;
}
// validate public key
if (publicKey.length !== 32) {
result.errorMsg = "Invalid publicKey length";
return result;
}
// include evidence with network prefix and multisig
const networkHex = getHexByNetwork(networkPrefix);
const netSigBytes = new Uint8Array([networkHex, "0x01"]); // network decimal + multisig
const evidence = blake.createHash("blake2b", {digestLength: 32}).update(publicKey).digest(); // hash it
const concatEvidence = Buffer.concat([netSigBytes, evidence], 34); // insert the publicKey
// get the hash of these 2, add first 4 bytes to the end.
const hashChecksumBuffer = blake.createHash("blake2b", {digestLength: 32}).update(concatEvidence).end().read().slice(0, 4);
const address = Buffer.concat([concatEvidence, hashChecksumBuffer], 38);
result.address = Base58.encode(address);
result.success = true;
return result;
}
/**
* Parse obj to retrieve addresses from the following keys:
* ["recipients", "sender", "changeAddress", "consolidationAdddress", "addresses"]
*
* @param {object} obj json obj to retrieve addresses from
* @returns {Array} list of addresses found in object
*/
function extractAddressesFromObj(obj) {
// only push unique items in array, so that validation is faster
let addresses = [];
if (obj.constructor === String) {
return [obj];
}
const addKeys = ["recipients", "sender", "changeAddress", "consolidationAdddress", "addresses"];
addKeys.forEach((addKey) => {
if (obj[addKey] && obj[addKey].length > 0) {
if (addKey === "recipients") {
obj[addKey].forEach((recipient) => {
// retrieve address from tuple
addresses = addresses.concat(recipient[0]);
});
} else {
addresses = addresses.concat(obj[addKey]);
}
}
});
return addresses;
}
/**
*
* @param {string} networkPrefix prefix of network where address will be used
* @param {string} address address to be used to create asset code
* @param {string} shortName name of assets, up to 8 bytes long latin-1 enconding
* @returns {string} return asset code
*/
function createAssetCode(networkPrefix, address, shortName) {
if (!isValidNetwork(networkPrefix)) {
throw new Error("Invalid network provided");
}
const validationResult = validateAddressesByNetwork(networkPrefix, address);
if (!validationResult.success) {
throw new Error("Invalid Addresses::" +
" Network Type: <" + this.networkPrefix + ">" +
" Invalid Addresses: <" + validationResult.invalidAddresses + ">" +
" Invalid Checksums: <" + validationResult.invalidChecksums + ">");
}
const decodedAddress = Base58.decode(address);
const slicedAddress = Buffer.from(decodedAddress.slice(0, 34));
// validate shortName
if (!shortName || shortName.length > 8) {
throw new Error("shortname must be defined with length up to 8 bytes in latin-1 encoding");
}
// ensure shortName is latin1
const latin1ShortName = Buffer.from(shortName, "latin1");
if (latin1ShortName.toString() !== shortName) {
throw new Error("shortname must be latin-1 encoding, other languages are currenlty not supported");
}
// concat 01 [version] + 34 bytes [address] + ^8bytes [asset name]
const version = new Uint8Array(["0x01"]);
const concatValues = Buffer.concat([version, slicedAddress, latin1ShortName], 43); // add trailing zeros, shortname must be 8 bytes long
const encodedAssetCode = Base58.encode(concatValues);
return encodedAssetCode;
}
/**
*
* @param {string} assetCode string in latin1 encoding
* @returns {boolean} true if valid
*/
function isValidAssetCode(assetCode) {
// concat 01 [version] + 34 bytes [address] + ^8bytes [asset name]
const decodedAssetCode = Base58.decode(assetCode);
if (decodedAssetCode.length !== 43 || decodedAssetCode[0] !== 1) {
return false;
}
return true;
}
/**
*
* @param {string} metadata string in latin1 encoding
* @returns {boolean} true if valid
*/
function isValidMetadata(metadata) {
// ensure data is latin1
if (!metadata) {
return false;
}
const latin1Buffer = Buffer.from(metadata, "latin1");
if (latin1Buffer.toString() !== metadata || latin1Buffer.length > 127) {
return false;
}
return true;
}
/**
* @param {String} networkPrefix prefix of network to validate against
* @returns {boolean} true if network is valid and is included in the valid networks obj
*/
function isValidNetwork(networkPrefix) {
return networkPrefix && validNetworks.includes(networkPrefix);
}
/**
* @param {String} networkPrefix prefix of network to validate against
* @returns {hex} hexadecimal value of network
*/
function getHexByNetwork(networkPrefix) {
return networksDefaults[networkPrefix].hex;
}
/**
* @param {String} networkPrefix prefix of network to validate against
* @returns {String} hexadecimal value of network
*/
function getDecimalByNetwork(networkPrefix) {
return networksDefaults[networkPrefix].decimal;
}
/**
* @param {String} networkPrefix prefix of network to validate against
* @returns {object} json obj of valid networks
*/
function getValidNetworksList() {
return validNetworks;
}
/**
*
* @param {string} address valid address to retrieve network prefix from
* @returns {object} obj with {success: <boolean>, networkPrefix: "<prefix if found>", error: "<message>"}
*/
function getAddressNetwork(address) {
const decodedAddress = Base58.decode(address);
const result = {
success: false,
networkPrefix: "",
error: ""
};
if (decodedAddress.length > 0) {
validNetworks.forEach((prefix) => {
if (networksDefaults[prefix].decimal === decodedAddress[0]) {
result.networkPrefix = prefix;
}
});
if (!isValidNetwork(result.networkPrefix)) {
result.success = false;
result.error = "invalid network prefix found";
} else {
result.success = true;
}
}
return result;
}
module.exports = {
isValidNetwork,
getHexByNetwork,
getDecimalByNetwork,
getValidNetworksList,
validateAddressesByNetwork,
generatePubKeyHashAddress,
createAssetCode,
isValidAssetCode,
isValidMetadata,
getAddressNetwork,
extractAddressesFromObj
};