Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
You can’t perform that action at this time.