Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gridplus-sdk",
"version": "1.2.1",
"version": "1.2.2",
"description": "SDK to interact with GridPlus Lattice1 device",
"scripts": {
"build": "tsc -p tsconfig.json",
Expand Down Expand Up @@ -81,4 +81,4 @@
"dist"
],
"license": "MIT"
}
}
85 changes: 70 additions & 15 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import bitwise from 'bitwise';
import { Byte, UInt4 } from 'bitwise/types';
import { KeyPair } from 'elliptic';
import { encode as rlpEncode } from 'rlp';
import superagent from 'superagent';
import bitcoin from './bitcoin';
import { sha256 } from 'hash.js/lib/hash/sha';
Expand Down Expand Up @@ -41,6 +40,8 @@ import {
promisifyCb,
toPaddedDER,
randomBytes,
isUInt4,
generateAppSecret,
} from './util';
const EMPTY_WALLET_UID = Buffer.alloc(32);

Expand All @@ -56,7 +57,7 @@ export class Client {
private baseUrl: string;
private name: string;
private key: KeyPair;
private privKey: Buffer;
private privKey: Buffer | string;
private retryCount: number;
private fwVersion: Buffer;
private skipRetryOnWrongWallet: boolean;
Expand Down Expand Up @@ -106,7 +107,7 @@ export class Client {
/** The name of the client. */
name?: string;
/** The private key of the client.*/
privKey?: Buffer;
privKey?: Buffer | string;
/** Number of times to retry a request if it fails. */
retryCount?: number;
/** The time to wait for a response before cancelling. */
Expand Down Expand Up @@ -192,6 +193,13 @@ export class Client {
return null;
}

/**
* `getAppName` returns the name of the application to which this device is currently paired.
*/
public getAppName (): string {
return this.name;
}

//=======================================================================
// LATTICE FUNCTIONS
//=======================================================================
Expand Down Expand Up @@ -262,13 +270,7 @@ export class Client {
// (RESP_ERR_PAIR_FAIL)
nameBuf.write(this.name);
}
// Make sure we add a null termination byte to the pairing secret
const preImage = Buffer.concat([
pubKey,
nameBuf,
Buffer.from(pairingSecret),
]);
const hash = Buffer.from(sha256().update(preImage).digest('hex'), 'hex');
const hash = generateAppSecret(pubKey, nameBuf, Buffer.from(pairingSecret));
const sig = this.key.sign(hash); // returns an array, not a buffer
const derSig = toPaddedDER(sig);
const payload = Buffer.concat([nameBuf, derSig]);
Expand Down Expand Up @@ -321,9 +323,9 @@ export class Client {
* @returns An array of addresses.
*/
public getAddresses (
opts: { startPath: number[], n: UInt4, flag: UInt4 },
opts: { startPath: number[], n: number, flag: number },
_cb?: (err?: string, data?: Buffer | string[]) => void,
): Promise<Buffer | string[]> {
): Promise<Buffer> {
return new Promise((resolve, reject) => {
const cb = promisifyCb(resolve, reject, _cb);
const MAX_ADDR = 10;
Expand All @@ -332,6 +334,9 @@ export class Client {
return cb('Please provide `startPath` and `n` options');
if (startPath.length < 2 || startPath.length > 5)
return cb('Path must include between 2 and 5 indices');
if (!isUInt4(n) || !isUInt4(flag)) {
return cb('Parameters `n` and `flag` must be integers between 0 and 15 inclusive');
}
if (n > MAX_ADDR)
return cb(`You may only request ${MAX_ADDR} addresses at once.`);

Expand Down Expand Up @@ -379,11 +384,11 @@ export class Client {
// `n` as a 4 bit value
flagVal =
fwConstants.getAddressFlags &&
fwConstants.getAddressFlags.indexOf(flag) > -1
? flag
fwConstants.getAddressFlags.indexOf(flag) > -1
? (flag as UInt4)
: 0;
const flagBits = bitwise.nibble.read(flagVal);
const countBits = bitwise.nibble.read(n);
const countBits = bitwise.nibble.read(n as UInt4);
val = bitwise.byte.write(flagBits.concat(countBits) as Byte);
} else {
// Very old firmware does not support this flag. We can deprecate this soon.
Expand Down Expand Up @@ -538,6 +543,13 @@ export class Client {
* data that can be used to decode some data in the future. The best example of this is the ABI
* defintion of a contract function. This definition is used to deserialize EVM calldata for
* future requests that call the specified function (as determined by the function selector).
*
* NOTE: The CRUD API to manage calldata decoders is written, but is currently
* compiled out of firmware to free up code space. For now we will leave
* these functions commented out.
* NOTE: You will need to re-enable `import { encode as rlpEncode } from 'rlp';`
*
* @deprecated
* @category Lattice
* @returns The decrypted response.
*/
Expand All @@ -547,6 +559,12 @@ export class Client {
): Promise<void> {
return new Promise((resolve, reject) => {
const cb = promisifyCb(resolve, reject, _cb);
// TODO: Update function comment if/when this is re-enabled.
return cb(
'Feature currently disabled in Lattice firmware. Please include ' +
'calldata decoder data in the signing request itself.'
);
/*
const { decoders, decoderType } = opts;
const fwConstants = getFwVersionConst(this.fwVersion);
if (!fwConstants.maxDecoderBufSz) {
Expand Down Expand Up @@ -575,11 +593,18 @@ export class Client {
return cb(null);
},
);
*/
});
}

/**
* `getDecoders` fetches a set of decoders saved on the target Lattice.
*
* NOTE: The CRUD API to manage calldata decoders is written, but is currently
* compiled out of firmware to free up code space. For now we will leave
* these functions commented out.
*
* @deprecated
* @category Lattice
* @returns The decrypted response.
*/
Expand All @@ -594,6 +619,12 @@ export class Client {
): Promise<{ decoders: Buffer[], total: number }> {
return new Promise((resolve, reject) => {
const cb = promisifyCb(resolve, reject, _cb);
// TODO: Update function comment if/when this is re-enabled.
return cb(
'Feature currently disabled in Lattice firmware. Please include ' +
'calldata decoder data in the signing request itself.'
);
/*
const { n = 1, startIdx = 0, skipTotal = false, decoderType } = opts;
const fwConstants = getFwVersionConst(this.fwVersion);
if (!fwConstants.maxDecoderBufSz) {
Expand Down Expand Up @@ -633,11 +664,19 @@ export class Client {
}
return cb(null, { decoders, total });
});
*/
});
}

/**
* `removeDecoders` requests removal of a set of decoders on the target Lattice.
*
* NOTE: The CRUD API to manage calldata decoders is written, but is currently
* compiled out of firmware to free up code space. For now we will leave
* these functions commented out.
* NOTE: You will need to re-enable `import { encode as rlpEncode } from 'rlp';`
*
* @deprecated
* @category Lattice
* @returns The decrypted response.
*/
Expand All @@ -647,6 +686,12 @@ export class Client {
): Promise<number> {
return new Promise((resolve, reject) => {
const cb = promisifyCb(resolve, reject, _cb);
// TODO: Update function comment if/when this is re-enabled.
return cb(
'Feature currently disabled in Lattice firmware. Please include ' +
'calldata decoder data in the signing request itself.'
);
/*
const { decoders, decoderType, rmAll = false } = opts;
const fwConstants = getFwVersionConst(this.fwVersion);
if (!fwConstants.maxDecoderBufSz) {
Expand Down Expand Up @@ -690,12 +735,17 @@ export class Client {
return cb(null, numRemoved);
},
);
*/
});
}

/**
* `addPermissionV0` takes in a currency, time window, spending limit, and decimals, and builds a
* payload to send to the Lattice.
*
* NOTE: This feature has been deprecated, but may be replaced in the future.
*
* @deprecated
* @category Lattice
*/
public addPermissionV0 (
Expand All @@ -710,6 +760,10 @@ export class Client {
): Promise<void> {
return new Promise((resolve, reject) => {
const cb = promisifyCb(resolve, reject, _cb);
return cb(
'This feature has been deprecated and may be replaced at a later time.'
);
/*
const { currency, timeWindow, limit, decimals, asset } = opts;
if (
!currency ||
Expand Down Expand Up @@ -752,6 +806,7 @@ export class Client {
return cb(null);
}
});
*/
});
}

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { CALLDATA as Calldata } from './calldata/index';
export { Client } from './client';
export { EXTERNAL as Constants } from './constants';
export { EXTERNAL as Utils } from './util'
96 changes: 93 additions & 3 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Static utility functions
import { Capability } from '@ethereumjs/tx';
import aes from 'aes-js';
import BN from 'bignumber.js';
import { BN } from 'bn.js';
import BigNum from 'bignumber.js';
import crc32 from 'crc-32';
import elliptic from 'elliptic';
import { sha256 } from 'hash.js/lib/hash/sha';
import { ecdsaRecover } from 'secp256k1';
import {
AES_IV,
BIP_CONSTANTS,
Expand All @@ -13,7 +17,10 @@ import {
} from './constants';
const { COINS, PURPOSES } = BIP_CONSTANTS;
const EC = elliptic.ec;
import isInteger from 'lodash/isInteger'
import inRange from 'lodash/inRange'
let ec;

//--------------------------------------------------
// LATTICE UTILS
//--------------------------------------------------
Expand Down Expand Up @@ -138,7 +145,7 @@ export const splitFrames = function(data, frameSz) {
}

function isBase10NumStr(x) {
const bn = new BN(x).toString().split('.').join('');
const bn = new BigNum(x).toString().split('.').join('');
const s = new String(x);
// Note that the JS native `String()` loses precision for large numbers, but we only
// want to validate the base of the number so we don't care about far out precision.
Expand All @@ -156,7 +163,7 @@ export const ensureHexBuffer = function(x, zeroIsNull = true) {
// Otherwise try to get this converted to a hex string
if (isNumber) {
// If this is a number or a base-10 number string, convert it to hex
x = `${new BN(x).toString(16)}`;
x = `${new BigNum(x).toString(16)}`;
} else if (typeof x === 'string' && x.slice(0, 2) === '0x') {
x = x.slice(2);
} else {
Expand Down Expand Up @@ -290,3 +297,86 @@ export const randomBytes = function(n) {
}
return buf;
}

/**
* `isUInt4` accepts a number and returns true if it is a UInt4
*/
export const isUInt4 = (n: number) => isInteger(n) && inRange(0, 16)

/**
* Generates an application secret for use in maintaining connection to device.
* @param {Buffer} deviceId - The device ID of the device you want to generate a token for.
* @param {Buffer} password - The password entered when connecting to the device.
* @param {Buffer} appName - The name of the application.
* @returns an application secret as a Buffer
*/
export const generateAppSecret = (
deviceId: Buffer,
password: Buffer,
appName: Buffer
): Buffer => {
const preImage = Buffer.concat([
deviceId,
password,
appName,
]);

return Buffer.from(sha256().update(preImage).digest('hex'), 'hex');
}

/**
* Generic signing does not return a `v` value like legacy ETH signing requests did.
* Get the `v` component of the signature as well as an `initV`
* parameter, which is what you need to use to re-create an @ethereumjs/tx
* object. There is a lot of tech debt in @ethereumjs/tx which also
* inherits the tech debt of ethereumjs-util.
* 1. The legacy `Transaction` type can call `_processSignature` with the regular
* `v` value.
* 2. Newer transaction types such as `FeeMarketEIP1559Transaction` will subtract
* 27 from the `v` that gets passed in, so we need to add `27` to create `initV`
* @param tx - An @ethereumjs/tx Transaction object
* @param resp - response from Lattice. Can be either legacy or generic signing variety
*/
export const getV = function (tx, resp) {
const hash = tx.getMessageToSign(true);
const rs = new Uint8Array(Buffer.concat([resp.sig.r, resp.sig.s]));
const pubkey = new Uint8Array(resp.pubkey);
const recovery0 = ecdsaRecover(rs, 0, hash, false);
const recovery1 = ecdsaRecover(rs, 1, hash, false);
const pubkeyStr = Buffer.from(pubkey).toString('hex');
const recovery0Str = Buffer.from(recovery0).toString('hex');
const recovery1Str = Buffer.from(recovery1).toString('hex');
let recovery;
if (pubkeyStr === recovery0Str) {
recovery = 0;
} else if (pubkeyStr === recovery1Str) {
recovery = 1;
} else {
return null;
}
// Newer transaction types just use the [0, 1] value
if (tx._type) {
return new BN(recovery);
}
// Legacy transactions should check for EIP155 support.
// In practice, virtually every transaction should have EIP155
// support since that hardfork happened in 2016...
// Various methods for fetching a chainID from different @ethereumjs/tx objects
let chainId = null;
if (tx.common && typeof tx.common.chainIdBN === 'function') {
chainId = tx.common.chainIdBN();
} else if (tx.chainId) {
chainId = new BN(tx.chainId);
}
if (!chainId || !tx.supports(Capability.EIP155ReplayProtection)) {
return new BN(recovery).addn(27);
}
// EIP155 replay protection is included in the `v` param
// and uses the chainId value.
return chainId.muln(2).addn(35).addn(recovery);
};

export const EXTERNAL = {
getV,
generateAppSecret
}
Loading