diff --git a/lib/plugins.js b/lib/plugins.js index 8565e56c..a9e0c27f 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -3,6 +3,7 @@ var fs = require('fs') var R = require('ramda') var async = require('async') +var HKDF = require('node-hkdf-sync') var BigNumber = require('bignumber.js') BigNumber.config({CRYPTO: true}) @@ -34,7 +35,7 @@ var idVerifierPlugin = null var infoPlugin = null var emailPlugin = null var smsPlugin = null -var masterSeed = null +var hkdf = null var currentlyUsedPlugins = {} @@ -54,9 +55,9 @@ var coins = { var alertFingerprint = null var lastAlertTime = null -// that's basically a constructor exports.init = function init () { - fs.readFileSync('seeds/seed.txt') + const masterSeed = fs.readFileSync('seeds/seed.txt').trim() + hkdf = new HKDF('sha256', 'lamassu-server-salt', masterSeed) } function loadPlugin (name, config) { @@ -175,15 +176,15 @@ exports.configure = function configure (config) { } ) - // WALLET [required] configure (or load) + // Give each crypto a different derived seed so as not to allow any + // plugin to spend another plugin's funds + const cryptoSeed = hkdf.derive(cryptoCode, 32) + loadOrConfigPlugin( walletPlugins[cryptoCode], 'transfer', cryptoCode, - { - nextCashOutSerialHD: db.nextCashOutSerialHD(cryptoCode), - masterSeed: masterSeed - }, + {masterSeed: cryptoSeed}, function onWalletChange (newWallet) { walletPlugins[cryptoCode] = newWallet pollBalance(cryptoCode) @@ -348,27 +349,25 @@ exports.sendCoins = function sendCoins (session, rawTx) { } exports.cashOut = function cashOut (session, tx) { - var tmpInfo = { - label: 'TX ' + Date.now(), - account: 'deposit' - } - var cryptoCode = tx.cryptoCode || 'BTC' var walletPlugin = walletPlugins[cryptoCode] - return new Promise((resolve, reject) => { + return db.nextCashOutSerialHD(tx.sessionId, cryptoCode) + .then(serialNumber => new Promise((resolve, reject) => { + const tmpInfo = { + label: 'TX ' + Date.now(), + account: 'deposit', + serialNumber: serialNumber + } + walletPlugin.newAddress(tmpInfo, function (err, address) { if (err) return reject(err) - const addressRec = R.is(String, address) - ? {address: address} - : address - - const newTx = R.assoc('toAddress', addressRec.address, tx) - return db.addInitialIncoming(session, newTx, addressRec) + const newTx = R.assoc('toAddress', address, tx) + return db.addInitialIncoming(session, newTx, address) .then(() => resolve(address)) }) - }) + })) } exports.dispenseAck = function dispenseAck (session, rec) { diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index 9f406fbd..0d3511e6 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -3,6 +3,7 @@ const BigNumber = require('bignumber.js') const pgp = require('pg-promise')() +const backoff = require('u-promised').backoff const logger = require('./logger') @@ -113,7 +114,7 @@ exports.sentCoins = function sentCoins (session, tx, toSend, fee, error, txHash) return db.none(sql, [txHash, error, session.id]) } -exports.addInitialIncoming = function addInitialIncoming (session, tx, addressRec) { +exports.addInitialIncoming = function addInitialIncoming (session, tx) { const fields = ['session_id', 'device_fingerprint', 'to_address', 'crypto_atoms', 'crypto_code', 'currency_code', 'fiat', 'tx_hash', 'phone', 'error' @@ -133,15 +134,6 @@ exports.addInitialIncoming = function addInitialIncoming (session, tx, addressRe ] return db.none(getInsertQuery('cash_out_txs', fields), values) - .then(() => { - const hd = addressRec.hd - if (hd) { - const fields2 = ['session_id', 'to_address', 'crypto_code', - 'hd_path_prefix', 'hd_serial'] - const values2 = [tx.sessionId, tx.toAddress, tx.cryptoCode, hd.pathPrefix, hd.serial] - return db.none(getInsertQuery('cash_out_hds', fields2), values2) - } - }) } function insertDispense (session, tx, cartridges) { @@ -434,9 +426,18 @@ exports.cacheResponse = function (session, path, method, body) { return db.none(sql, values) } -exports.nextCashOutSerialHD = function nextCashOutSerialHD (cryptoCode) { +exports.nextCashOutSerialHD = function nextCashOutSerialHD (sessionId, cryptoCode) { const sql = `select hd_serial from cash_out_hds where crypto_code=$1 order by hd_serial desc limit 1` - return db.oneOrNone(sql, [cryptoCode]) - .then(row => row ? row.hd_serial + 1 : 0) + const attempt = db.oneOrNone(sql, [cryptoCode]) + .then(row => { + const serialNumber = row ? row.hd_serial + 1 : 0 + const fields2 = ['session_id', 'crypto_code', 'hd_serial'] + const sql2 = getInsertQuery('cash_out_hds', fields2) + const values2 = [sessionId, cryptoCode, serialNumber] + return db.none(sql2, values2) + .then(() => serialNumber) + }) + + return backoff(100, 0, 5, attempt) } diff --git a/migrations/012-add-hd-path-serial.js b/migrations/012-add-hd-path-serial.js index b2b84700..2beb4b70 100644 --- a/migrations/012-add-hd-path-serial.js +++ b/migrations/012-add-hd-path-serial.js @@ -4,12 +4,10 @@ exports.up = function (next) { var sql = [ `create table cash_out_hds ( session_id uuid PRIMARY KEY, - to_address text NOT NULL, crypto_code text NOT NULL, - hd_path_prefix text NOT NULL, hd_serial integer NOT NULL, created timestamptz NOT NULL default now(), - unique (crypto_code, hd_serial) + unique (crypto_code, hd_serial), )` ] db.multi(sql, next) diff --git a/package.json b/package.json index 89bd1b90..f351d393 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,14 @@ "lamassu-smtp2go": "^1.0.3", "lamassu-twilio": "^1.1.0", "minimist": "0.0.8", + "node-hkdf-sync": "^1.0.0", "node-uuid": "^1.4.2", "numeral": "^1.5.3", "pg": "^4.5.5", "pg-promise": "^4.3.3", "pretty-ms": "^2.1.0", "ramda": "^0.21.0", + "u-promised": "^0.2.4", "wreck": "5.1.0" }, "repository": { diff --git a/todo.txt b/todo.txt index 01ebf149..ade6827a 100644 --- a/todo.txt +++ b/todo.txt @@ -1,4 +1,3 @@ - l-m shouldn't keep polling l-s when not on pending screen (low priority) -- need more bulletproof way to ensure only unique serials for each coin -- use tx to read last, then insert new row +- scrutinize hkdf, maybe use own simplified version