Last active
October 17, 2019 04:19
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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