Skip to content

Instantly share code, notes, and snippets.

@nsjames
Last active October 17, 2019 04:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nsjames/44d9c18879342449a211c49e53c74ccb to your computer and use it in GitHub Desktop.
Save nsjames/44d9c18879342449a211c49e53c74ccb to your computer and use it in GitHub Desktop.
An example of how to protect your dapp and keys when assuming resource control for your users.
// SECURITY CONCERNS!
// Because your backend will be signing transactions, you will need to validate
// the transaction details before actually signing them.
// Using keys designated for specific actions is okay in some situations, but not all
// (like when your dapp wants to pay for eosio.token transfers, which exposes you to risk of losing your available tokens)
// The best flow for this is the following:
// VISUAL DIAGRAM: https://t.me/ScatterDevelopers/16787
// - Front-end which implements a multi-signer (scatter + api signer)
// - Scatter signs for the user
// - Your signature request goes to your API, which is only a "FILTER"
// - The actual signing machine should be a private network linked machine that only accepts
// requests from the filtering API. That way the keys don't even live on the same machine
// that users are exposed to.
// EXAMPLE OF AN API BASED SIGNER FOR THE FRONT END
// This simply forwards requests from the front end to your filtering API.
const apiSigner = {
getAvailableKeys:async () => fetch('https://your.api/keys'),
sign:async (signargs) => fetch('https://your.api/sign', {
method:"POST",
body:JSON.stringify(signargs)
}),
};
// You can use this like so:
const signatureProvider = ScatterJS.eosMultiHook(network, [apiSigner]);
const api = new Api({ rpc, signatureProvider });
// EXAMPLE OF AN API FILTER
// You can pop this onto a backend API server and use it to filter incoming requests.
// !!!!!!!!!!!! THIS IS UNTESTED EXAMPLE CODE, PLEASE MAKE SURE IT IS DOING WHAT YOU THINK IT IS !!!!!!!!!!!!
// eosjs bruv https://github.com/EOSIO/eosjs/
import { Api } from 'eosjs';
// Used for binary conversion https://github.com/EOSIO/eosjs/blob/master/src/eosjs-numeric.ts#L181
import * as numeric from "eosjs/dist/eosjs-numeric";
// Used for proofing the filtering api https://github.com/EOSIO/eosjs-ecc
import ecc from 'eosjs-ecc';
const PROOFING_KEY = 'EOSIO KEY: LOAD THIS INTO THE MACHINE WHILE THE PROCESS IS RUNNING USING PROMPTS, OR USE A .env FILE WHICH IS CLEARED AFTER THE PROCESS IS RUNNING';
const filter = async signargs => {
const YOUR_TARGET_CHAIN = 'aca...'; // You should know which chain you support
const YOUR_ACCOUNT = 'hello.world'; // You should know which account you are receiving things to in the case of transfers
const TOTAL_ACTIONS = 1; // You should know how many actions you sent from your front end
const VALID_CONTRACTS = ['eosio.token', 'somethingelse']; // You should know which contracts your users are able to interact with.
if(signargs.chainId !== YOUR_TARGET_CHAIN) return console.error('This transaction is not for your chain.');
const api = Api({...}); // An eosjs2 API object
const contractAccounts = signargs.abis
// Gets the contract account names from the ABIs
.map(x => x.accountName)
// Makes the array distinct
.reduce((acc,x) => {
if(!acc.includes(x)) acc.push(x);
return acc;
}, []);
// https://t.me/ScatterDevelopers/16795
// As Cesar mentioned, this might not be necessary since the `deserializeTransactionWithActions` below will
// potentially fetch missing ABIs, however you can move this code into your API's initialization stage
// to pre-fill a re-usable eosjs2 `api` object so that you don't have to waste network resources for fetching
// abis every time you want to validate.
// Keep in mind that if a contract you are servicing is updated, you will need to bust caches on your eosjs2 object.
for(let i = 0; i < contractAccounts.length; i++){
const account = contractAccounts[i];
// Gets the raw ABI from the chain (DONT RELY ON THE ABI THAT YOU GOT FROM THE `signargs`!)
const chainAbi = await fetch(`https://nodes.get-scatter.com/v1/chain/get_raw_abi`, {
method:"POST",
body:JSON.stringify({account_name:account})
}).then(x => x.json()).then(x => x.abi).catch(() => null);
if(!chainAbi) return console.error('Could not fetch ABI from chain');
// Binary representation
const rawAbi = numeric.base64ToBinary(chainAbi);
// JSON representation
const abi = api.rawAbiToJson(rawAbi);
// Caches the ABIs into your instance of eosjs2
api.cachedAbis.set(account, { rawAbi, abi });
}
// Converting the hex signargs back into a buffer
const buffer = Buffer.from(signargs.serializedTransaction, 'hex');
// Deserializes the serialized transaction into usable JSON
const parsed = await api.deserializeTransactionWithActions(buffer);
const {actions} = parsed;
if(actions.length !== TOTAL_ACTIONS)
return console.error('There were either too little or too many actions.');
for(let i = 0; i < actions.length; i++){
const action = actions[i];
const {account, name, data} = action;
// YOUR CUSTOM LOGIC HERE!
if(!VALID_CONTRACTS.includes(account)) return console.error(`This contract isn't supported: `, data);
if(account === 'eosio.token'){
if(name !== 'transfer') return console.error('Only transfers are allowed on this contract: ', data);
if(data.to !== YOUR_ACCOUNT || data.from === YOUR_ACCOUNT) return console.error(`This transfer wasn't sending EOS to you, or was sending to you.`, data);
}
}
// Recreating a signable signature from the transaction we've validated.
// Don't assume anything that came from the signargs is good to sign.
// (even though in the case of eosjs2 this is made directly from `signargs.serializedTransaction`)
const toSign = Buffer.concat([
new Buffer(signargs.chainId, "hex"), // Chain ID
buffer, // Transaction
new Buffer(new Uint8Array(32)), // Context free actions
]);
// Though this is good, I'd have an authentication key on this filtering API
// which only exists on running scope, and is used to also sign this and prove
// it came from the filtering machine's running process, and not someone who
// might have gained access to the filtering machine but doesn't have the proofing key.
const signedProof = ecc.sign(toSign, PROOFING_KEY);
// Send the transaction to sign + the proof this machine executed filtering to
// your other privately networked signing machine.
return signWithOtherMachine(toSign, signedProof);
}
// FOR THE SIGNING MACHINE:
// - validate that the IP the request originated from is a networking machine
// (in the case that there are other servers in that private network)
// - check that the `signedProof` matches the known public key:
// `if(ecc.recover(signedProof, toSign) !== KNOWN_PUBLIC_KEY) return false;`
// - sign the `toSign`:
// `ecc.sign(toSign, SECURE_KEY)`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment