gridplus-sdk
Advanced tools
Comparing version 0.8.7 to 0.9.0
{ | ||
"name": "gridplus-sdk", | ||
"version": "0.8.7", | ||
"version": "0.9.0", | ||
"description": "SDK to interact with GridPlus Lattice1 device", | ||
@@ -17,3 +17,5 @@ "scripts": { | ||
"test-btc": "mocha --timeout 180000 -R spec test/testBtc.js --recursive --exit", | ||
"test-wallet-jobs": "mocha --timeout 300000 -R spec test/testWalletJobs.js --recursive --exit" | ||
"test-kv": "mocha --timeout 180000 -R spec test/testKv.js --recursive --exit", | ||
"test-wallet-jobs": "mocha --timeout 300000 -R spec test/testWalletJobs.js --recursive --exit", | ||
"test-sigs": "mocha --timeout 300000 -R spec test/testSigs.js --recursive --exit" | ||
}, | ||
@@ -20,0 +22,0 @@ "main": "./index.js", |
@@ -21,2 +21,3 @@ const bitwise = require('bitwise'); | ||
const { | ||
ASCII_REGEX, | ||
getFwVersionConst, | ||
@@ -250,3 +251,2 @@ ADDR_STR_LEN, | ||
} | ||
// Build the payload | ||
@@ -370,3 +370,3 @@ const payload = Buffer.alloc(2 + fwConstants.reqMaxDataSz); | ||
// Correct wallet and no errors -- handle the response | ||
const d = this._handleEncResponse(res, decResLengths.finalizePair); | ||
const d = this._handleEncResponse(res, decResLengths.empty); | ||
if (d.err) | ||
@@ -379,2 +379,157 @@ return cb(d.err); | ||
getKvRecords(opts, cb) { | ||
const { type = 0, n = 1, start = 0 } = opts; | ||
const fwConstants = getFwVersionConst(this.fwVersion); | ||
if (!fwConstants.kvActionsAllowed) { | ||
return cb('Unsupported. Please update firmware.'); | ||
} else if (n < 1) { | ||
return cb('You must request at least one record.'); | ||
} else if (n > fwConstants.kvActionMaxNum) { | ||
return cb(`You may only request up to ${fwConstants.kvActionMaxNum} records at once.`); | ||
} | ||
const payload = Buffer.alloc(9); | ||
payload.writeUInt32LE(type); | ||
payload.writeUInt8(n, 4); | ||
payload.writeUInt32LE(start, 5); | ||
// Encrypt the request and send it to the Lattice. | ||
const param = this._buildEncRequest(encReqCodes.GET_KV_RECORDS, payload); | ||
return this._request(param, (err, res, responseCode) => { | ||
if (responseCode === responseCodes.RESP_ERR_WALLET_NOT_PRESENT) { | ||
// If we catch a case where the wallet has changed, try getting the new active wallet | ||
// and recursively make the original request. | ||
this._getActiveWallet((err) => { | ||
if (err) return cb(err) | ||
else return this.getKvRecords(opts, cb); | ||
}) | ||
} else if (err) { | ||
// If there was another error caught, return it | ||
if (err) return cb(err); | ||
} else { | ||
// Correct wallet and no errors -- handle the response | ||
const d = this._handleEncResponse(res, decResLengths.getKvRecords); | ||
if (d.err) | ||
return cb(d.err); | ||
// Decode the response | ||
let off = 65; // Skip 65 byte pubkey prefix | ||
const nTotal = parseInt(d.data.slice(off, off+4).toString('hex'), 16); off += 4; | ||
const nFetched = parseInt(d.data.slice(off, off+1).toString('hex'), 16); off += 1; | ||
if (nFetched > fwConstants.kvActionMaxNum) | ||
return cb('Too many records fetched. Firmware error.'); | ||
const records = []; | ||
for (let i = 0; i < nFetched; i++) { | ||
const r = {}; | ||
r.id = parseInt(d.data.slice(off, off + 4).toString('hex'), 16); off += 4; | ||
r.type = parseInt(d.data.slice(off, off + 4).toString('hex'), 16); off += 4; | ||
r.caseSensitive = parseInt(d.data.slice(off, off + 1).toString('hex'), 16) === 1 ? true : false; off += 1; | ||
const keySz = parseInt(d.data.slice(off, off + 1).toString('hex'), 16); off += 1; | ||
r.key = d.data.slice(off, off + keySz-1).toString(); off += (fwConstants.kvKeyMaxStrSz + 1); | ||
const valSz = parseInt(d.data.slice(off, off + 1).toString('hex'), 16); off += 1; | ||
r.val = d.data.slice(off, off + valSz-1).toString(); off += (fwConstants.kvValMaxStrSz + 1); | ||
records.push(r); | ||
} | ||
return cb(null, { records, total: nTotal, fetched: nFetched }); | ||
} | ||
}) | ||
} | ||
addKvRecords(opts, cb) { | ||
const { type = 0, records = {}, caseSensitive=false } = opts; | ||
const fwConstants = getFwVersionConst(this.fwVersion); | ||
if (!fwConstants.kvActionsAllowed) { | ||
return cb('Unsupported. Please update firmware.'); | ||
} else if (typeof records !== 'object' || Object.keys(records).length === 0) { | ||
return cb('One or more key-value mapping must be provided in `records` param.'); | ||
} else if (Object.keys(records).length > fwConstants.kvActionMaxNum) { | ||
return cb(`Too many keys provided. Please only provide up to ${fwConstants.kvActionMaxNum}.`); | ||
} else if (Object.keys(records).length < 1) { | ||
return cb('You must provide at least one key to add.') | ||
} | ||
const payload = Buffer.alloc(1 + (139 * fwConstants.kvActionMaxNum)); | ||
payload.writeUInt8(Object.keys(records).length); | ||
let off = 1; | ||
try { | ||
Object.keys(records).forEach((key) => { | ||
if (typeof key !== 'string' || String(key).length > fwConstants.kvKeyMaxStrSz) { | ||
throw new Error(`Key ${key} too large. Must be <=${fwConstants.kvKeyMaxStrSz} characters.`); | ||
} else if (typeof records[key] !== 'string' || String(records[key]).length > fwConstants.kvValMaxStrSz) { | ||
throw new Error(`Value ${records[key]} too large. Must be <$={fwConstants.kvValMaxStrSz} characters.`); | ||
} else if (String(key).length === 0 || String(records[key]).length === 0) { | ||
throw new Error('Keys and values must be >0 characters.'); | ||
} else if (!ASCII_REGEX.test(key) || !ASCII_REGEX.test(records[key])) { | ||
throw new Error('Unicode characters are not supported.'); | ||
} | ||
// Skip the ID portion. This will get added by firmware. | ||
payload.writeUInt32LE(0, off); off += 4; | ||
payload.writeUInt32LE(type, off); off += 4; | ||
payload.writeUInt8(caseSensitive === true, off); off += 1; | ||
payload.writeUInt8(String(key).length + 1, off); off += 1; | ||
Buffer.from(String(key)).copy(payload, off); off += (fwConstants.kvKeyMaxStrSz + 1); | ||
payload.writeUInt8(String(records[key]).length + 1, off); off += 1; | ||
Buffer.from(String(records[key])).copy(payload, off); off += (fwConstants.kvValMaxStrSz + 1); | ||
}) | ||
} catch (err) { | ||
return cb(`Error building request: ${err.message}`); | ||
} | ||
// Encrypt the request and send it to the Lattice. | ||
const param = this._buildEncRequest(encReqCodes.ADD_KV_RECORDS, payload); | ||
return this._request(param, (err, res, responseCode) => { | ||
if (responseCode === responseCodes.RESP_ERR_WALLET_NOT_PRESENT) { | ||
// If we catch a case where the wallet has changed, try getting the new active wallet | ||
// and recursively make the original request. | ||
this._getActiveWallet((err) => { | ||
if (err) return cb(err) | ||
else return this.addKvRecords(opts, cb); | ||
}) | ||
} else if (err) { | ||
// If there was another error caught, return it | ||
if (err) return cb(err); | ||
} else { | ||
// Correct wallet and no errors -- handle the response | ||
const d = this._handleEncResponse(res, decResLengths.empty); | ||
if (d.err) | ||
return cb(d.err); | ||
return cb(null); | ||
} | ||
}) | ||
} | ||
removeKvRecords(opts, cb) { | ||
const { type = 0, ids = [] } = opts; | ||
const fwConstants = getFwVersionConst(this.fwVersion); | ||
if (!fwConstants.kvActionsAllowed) { | ||
return cb('Unsupported. Please update firmware.'); | ||
} else if (!Array.isArray(ids) || ids.length < 1) { | ||
return cb('You must include one or more `ids` to removed.') | ||
} else if (ids.length > fwConstants.kvRemoveMaxNum) { | ||
return cb(`Only up to ${fwConstants.kvRemoveMaxNum} records may be removed at once.`) | ||
} | ||
const payload = Buffer.alloc(5 + (4 * fwConstants.kvRemoveMaxNum)); | ||
payload.writeUInt32LE(type); | ||
payload.writeUInt8(ids.length, 4); | ||
for (let i = 0; i < ids.length; i++) { | ||
payload.writeUInt32LE(ids[i], 5 + (4 * i)) | ||
} | ||
// Encrypt the request and send it to the Lattice. | ||
const param = this._buildEncRequest(encReqCodes.REMOVE_KV_RECORDS, payload); | ||
return this._request(param, (err, res, responseCode) => { | ||
if (responseCode === responseCodes.RESP_ERR_WALLET_NOT_PRESENT) { | ||
// If we catch a case where the wallet has changed, try getting the new active wallet | ||
// and recursively make the original request. | ||
this._getActiveWallet((err) => { | ||
if (err) return cb(err) | ||
else return this.removeKvRecords(opts, cb); | ||
}) | ||
} else if (err) { | ||
// If there was another error caught, return it | ||
if (err) return cb(err); | ||
} else { | ||
// Correct wallet and no errors -- handle the response | ||
const d = this._handleEncResponse(res, decResLengths.empty); | ||
if (d.err) | ||
return cb(d.err); | ||
return cb(null); | ||
} | ||
}) | ||
} | ||
//======================================================================= | ||
@@ -433,3 +588,2 @@ // INTERNAL FUNCTIONS | ||
const ephemId = parseInt(this._getEphemId().toString('hex'), 16) | ||
// Build the payload and checksum | ||
@@ -578,3 +732,3 @@ const payloadPreCs = Buffer.concat([Buffer.from([enc_request_code]), payload]); | ||
_handlePair(encRes) { | ||
const d = this._handleEncResponse(encRes, decResLengths.finalizePair); | ||
const d = this._handleEncResponse(encRes, decResLengths.empty); | ||
if (d.err) return d.err; | ||
@@ -581,0 +735,0 @@ // Remove the pairing salt - we're paired! |
@@ -12,3 +12,3 @@ // Consistent with Lattice's IV | ||
const decResLengths = { | ||
finalizePair: 0, // Only contains the pubkey | ||
empty: 0, // Only contains the pubkey | ||
getAddresses: 10 * ADDR_STR_LEN, // 10x 129 byte strings (128 bytes + null terminator) | ||
@@ -18,2 +18,3 @@ sign: 1090, // 1 DER signature for ETH, 10 for BTC + change pubkeyhash | ||
addAbiDefs: 8, | ||
getKvRecords: 1395, | ||
test: 1646 // Max size of test response payload | ||
@@ -53,10 +54,13 @@ } | ||
const encReqCodes = { | ||
'FINALIZE_PAIRING': 0x00, | ||
'GET_ADDRESSES': 0x01, | ||
'ADD_PERMISSION': 0x02, | ||
'SIGN_TRANSACTION': 0x03, | ||
'GET_WALLETS': 0x04, | ||
'ADD_PERMISSION_V0': 0x05, | ||
'ADD_ABI_DEFS': 0x06, | ||
'TEST': 0x07, | ||
'FINALIZE_PAIRING': 0, | ||
'GET_ADDRESSES': 1, | ||
'ADD_PERMISSION': 2, | ||
'SIGN_TRANSACTION': 3, | ||
'GET_WALLETS': 4, | ||
'ADD_PERMISSION_V0': 5, | ||
'ADD_ABI_DEFS': 6, | ||
'GET_KV_RECORDS': 7, | ||
'ADD_KV_RECORDS': 8, | ||
'REMOVE_KV_RECORDS': 9, | ||
'TEST': 10, | ||
} | ||
@@ -308,3 +312,23 @@ | ||
// V0.10.12 allows new ETH transaction types | ||
// V0.11.5 added an API for creating, removing, and fetching key-val file | ||
// records. For the purposes of this SDK, we only hook into one type of kv | ||
// file: address names. | ||
if (!legacy && gte(v, [0, 12, 0])) { | ||
c.kvActionsAllowed = true; | ||
c.kvKeyMaxStrSz = 63; | ||
c.kvValMaxStrSz = 63; | ||
c.kvActionMaxNum = 10; | ||
c.kvRemoveMaxNum = 100; | ||
} | ||
// V0.11.2 changed how messages are displayed. For personal_sign messages | ||
// we now write the header (`Signer: <path>`) into the main body of the screen. | ||
// This means personal sign message max size is slightly smaller than for | ||
// EIP712 messages because in the latter case there is no header | ||
// Note that `<path>` has max size of 62 bytes (`m/X/X/...`) | ||
if (!legacy && gte(v, [0, 11, 2])) { | ||
c.personalSignHeaderSz = 72; | ||
} | ||
// V0.11.0 allows new ETH transaction types | ||
if (!legacy && gte(v, [0, 11, 0])) { | ||
@@ -346,2 +370,3 @@ c.allowedEthTxTypesVersion = 1; | ||
module.exports = { | ||
ASCII_REGEX: (/^[\x00-\x7F]+$/), | ||
getFwVersionConst, | ||
@@ -348,0 +373,0 @@ ADDR_STR_LEN, |
@@ -68,3 +68,2 @@ // Utils for Ethereum transactions. This is effecitvely a shim of ethereumjs-util, which | ||
const VAR_PATH_SZ = fwConstants.varAddrPathSzAllowed; | ||
// Sanity checks: | ||
@@ -110,3 +109,2 @@ // There are a handful of named chains we allow the user to reference (`chainIds`) | ||
//-------------- | ||
// Ensure all fields are 0x-prefixed hex strings | ||
@@ -428,10 +426,2 @@ const rawTx = []; | ||
function isHexStr(str) { | ||
return (/^[0-9a-fA-F]+$/).test(str) | ||
} | ||
function isASCIIStr(str) { | ||
return (/^[\x00-\x7F]+$/).test(str) | ||
} | ||
// Determine if the Lattice can display a string we give it. Currently, the Lattice can only | ||
@@ -580,3 +570,3 @@ // display ASCII strings, so we will reject other UTF8 codes. | ||
payload = ensureHexBuffer(input.payload) | ||
displayHex = false === isASCIIStr(Buffer.from(input.payload.slice(2), 'hex').toString()) | ||
displayHex = false === constants.ASCII_REGEX.test(Buffer.from(input.payload.slice(2), 'hex').toString()) | ||
} else { | ||
@@ -598,6 +588,10 @@ if (false === latticeCanDisplayStr(input.payload)) | ||
throw new Error('Unsupported input data type'); | ||
displayHex = false === isASCIIStr(input.payload.toString()) | ||
displayHex = false === constants.ASCII_REGEX.test(input.payload.toString()) | ||
} | ||
const fwConst = input.fwConstants; | ||
const maxSzAllowed = MAX_BASE_MSG_SZ + (fwConst.extraDataMaxFrames * fwConst.extraDataFrameSz); | ||
let maxSzAllowed = MAX_BASE_MSG_SZ + (fwConst.extraDataMaxFrames * fwConst.extraDataFrameSz); | ||
if (fwConst.personalSignHeaderSz) { | ||
// Account for the personal_sign header string | ||
maxSzAllowed -= fwConst.personalSignHeaderSz; | ||
} | ||
if (fwConst.ethMsgPreHashAllowed && payload.length > maxSzAllowed) { | ||
@@ -604,0 +598,0 @@ // If this message will not fit and pre-hashing is allowed, do that |
@@ -150,3 +150,2 @@ // Static utility functions | ||
module.exports = { | ||
@@ -153,0 +152,0 @@ isValidAssetPath, |
122658
2746