ledger-cosmos-js
Advanced tools
Comparing version 1.0.0 to 2.0.2
{ | ||
"name": "ledger-cosmos-js", | ||
"version": "1.0.0", | ||
"description": "Node API for Ledger Nano S Cosmos App", | ||
"main": "./src/index.js", | ||
"version": "2.0.2", | ||
"description": "Node API for Cosmos App (Ledger Nano S/X)", | ||
"main": "./dist/index.js", | ||
"repository": { | ||
@@ -12,2 +12,4 @@ "type": "git", | ||
"Ledger", | ||
"Ledger", | ||
"Ledger", | ||
"Javascript", | ||
@@ -24,26 +26,35 @@ "Tendermint", | ||
"dependencies": { | ||
"@ledgerhq/hw-transport": "^4.35.0", | ||
"@babel/runtime": "^7.4.4", | ||
"bech32": "^1.1.3", | ||
"ripemd160": "^2.0.2", | ||
"node-hid": "0.7.4", | ||
"q": "^1.5.1" | ||
"ripemd160": "^2.0.2" | ||
}, | ||
"devDependencies": { | ||
"browserify": "^16.2.3", | ||
"chai": "^4.2.0", | ||
"mocha": "^5.0.1", | ||
"secp256k1": "^3.6.2", | ||
"uglify-js": "^3.5.8" | ||
"@ledgerhq/hw-transport-node-hid": "^4.35.0", | ||
"@ledgerhq/logs": "^4.60.2", | ||
"@babel/cli": "^7.4.4", | ||
"@babel/core": "^7.4.4", | ||
"@babel/node": "^7.2.2", | ||
"@babel/plugin-transform-runtime": "^7.4.4", | ||
"@babel/preset-env": "^7.4.4", | ||
"babel-jest": "^24.7.1", | ||
"bip32": "2.0.1", | ||
"bip39": "2.6.0", | ||
"crypto-js": "3.1.9-1", | ||
"eslint": "^5.16.0", | ||
"eslint-config-airbnb-base": "^13.1.0", | ||
"eslint-plugin-import": "^2.17.2", | ||
"eslint-plugin-jest": "^22.5.1", | ||
"jest": "^24.7.1", | ||
"secp256k1": "^3.6.2" | ||
}, | ||
"scripts": { | ||
"browserify": "browserify --im --s=ledger -o browser/ledger.js src/index-browserify.js; cp src/chrome-u2f-api.js browser/; cp browser/*.js tests/browser/", | ||
"browserify-test": "browserify --standalone runExample -o tests/browser/test-bundle.js tests/tests-browser.js", | ||
"test-node": "mocha tests/tests-node.js", | ||
"clean": "rm -f browser/*.js; rm -f tests/browser/*.js;" | ||
"build": "babel src --out-dir dist", | ||
"test:unit": "jest -c jest.config.unit.js", | ||
"test:integration": "jest -c jest.config.integration.js --runInBand --detectOpenHandles" | ||
}, | ||
"browser": { | ||
"node-hid": false | ||
}, | ||
"directories": { | ||
"test": "tests" | ||
} | ||
"moduleDirectories": [ | ||
"node_modules", | ||
"dist" | ||
] | ||
} |
# ledger-cosmos-js | ||
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) | ||
[![npm version](https://badge.fury.io/js/ledger-cosmos-js.svg)](https://badge.fury.io/js/ledger-cosmos-js) | ||
[![CircleCI](https://circleci.com/gh/ZondaX/ledger-cosmos-js/tree/master.svg?style=shield)](https://circleci.com/gh/ZondaX/ledger-cosmos-js/tree/master) | ||
This package provides a basic client library to communicate with a Tendermint/Cosmos App running in a Ledger Nano S | ||
This package provides a basic client library to communicate with a Tendermint/Cosmos App running in a Ledger Nano S/X | ||
There are two running modes: | ||
The package is being published in npmjs. It is recommended to use that in order to receive updates and fixes. | ||
- *HID*: Direct access via HID. This can be used from a backend, node.js, etc. | ||
- *U2F*: This allows access to the device from the browser (client side) | ||
# Testing | ||
Install all dependencies by running | ||
``` | ||
npm install | ||
``` | ||
There are a few useful scripts: | ||
- ```npm test```: Will run HID tests | ||
- ```npm browserify```: Will generate js files that are necessary for U2F/browser integration | ||
- ```npm browserify-test```: Will generate js files that are necessary for browser testing. After executing this script. You can access `tests/browser/index.html` to run browser tests. | ||
Warning: You need to setup a webserver and point it to index.html. U2F communication requires an https connection. | ||
## How to run browser tests | ||
First you need to deploy the javascript files. Run the following: | ||
```bash | ||
npm run browserify | ||
npm run browserify-test | ||
``` | ||
now go to the test directory and run `caddy` | ||
```bash | ||
cd tests/browser | ||
caddy | ||
``` | ||
you should see something like: | ||
```text | ||
Activating privacy features... done. | ||
https://localhost:2020 | ||
WARNING: File descriptor limit 1024 is too low for production servers. At least 8192 is recommended. Fix with `ulimit -n 8192`. | ||
``` | ||
Open your browser at `https://localhost:2020` and you can now run the test |
385
src/index.js
@@ -1,25 +0,366 @@ | ||
/******************************************************************************** | ||
* Ledger Node JS API | ||
* (c) 2016-2017 Ledger | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
********************************************************************************/ | ||
/** ****************************************************************************** | ||
* (c) 2019 ZondaX GmbH | ||
* (c) 2016-2017 Ledger | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
******************************************************************************* */ | ||
var ledger = module.exports; | ||
import Transport from '@ledgerhq/hw-transport'; | ||
import { TransportStatusError } from '@ledgerhq/hw-transport'; | ||
import crypto from 'crypto'; | ||
import Ripemd160 from 'ripemd160'; | ||
import bech32 from 'bech32'; | ||
ledger.comm_node = require('./ledger-comm-node'); | ||
ledger.comm_u2f = require('./ledger-comm-u2f'); | ||
ledger.App = require('./ledger-app'); | ||
ledger.Tools = require('./tools'); | ||
const CLA = 0x55; | ||
const CHUNK_SIZE = 250; | ||
module.exports = ledger; | ||
const INS = { | ||
GET_VERSION: 0x00, | ||
PUBLIC_KEY_SECP256K1: 0x01, | ||
SIGN_SECP256K1: 0x02, | ||
SHOW_ADDR_SECP256K1: 0x03, | ||
GET_ADDR_SECP256K1: 0x04, | ||
}; | ||
const ERROR_DESCRIPTION = { | ||
1: "U2F: Unknown", | ||
2: "U2F: Bad request", | ||
3: "U2F: Configuration unsupported", | ||
4: "U2F: Device Ineligible", | ||
5: "U2F: Timeout", | ||
14: "Timeout", | ||
0x9000: "No errors", | ||
0x9001: "Device is busy", | ||
0x6400: "Execution Error", | ||
0x6700: "Wrong Length", | ||
0x6982: "Empty Buffer", | ||
0x6983: "Output buffer too small", | ||
0x6984: "Data is invalid", | ||
0x6985: "Conditions not satisfied", | ||
0x6986: "Transaction rejected", | ||
0x6A80: "Bad key handle", | ||
0x6B00: "Invalid P1/P2", | ||
0x6D00: "Instruction not supported", | ||
0x6E00: "Cosmos app does not seem to be open", | ||
0x6F00: "Unknown error", | ||
0x6F01: "Sign/verify error", | ||
}; | ||
function errorCodeToString(statusCode) { | ||
if (statusCode in ERROR_DESCRIPTION) | ||
return ERROR_DESCRIPTION[statusCode]; | ||
return `Unknown Status Code: ${statusCode}` | ||
} | ||
export function getBech32FromPK(hrp, pk) { | ||
if (pk.length !== 33) { | ||
throw new Error("expected compressed public key [31 bytes]"); | ||
} | ||
const hash_sha256 = crypto.createHash('sha256').update(pk).digest(); | ||
const hash_rip = new Ripemd160().update(hash_sha256).digest(); | ||
return bech32.encode(hrp, bech32.toWords(hash_rip)); | ||
} | ||
function processErrorResponse(response) { | ||
return { | ||
return_code: response.statusCode, | ||
error_message: errorCodeToString(response.statusCode) | ||
}; | ||
} | ||
function serializeHRP(hrp) { | ||
if (hrp == null || hrp.length < 3 || hrp.length > 83) { | ||
throw new Error("Invalid HRP") | ||
} | ||
let buf = Buffer.alloc(1 + hrp.length); | ||
buf.writeUInt8(hrp.length, 0); | ||
buf.write(hrp, 1); | ||
return buf; | ||
} | ||
function serializePath(path) { | ||
if (path == null || path.length < 3) { | ||
throw new Error("Invalid path.") | ||
} | ||
if (path.length > 10) { | ||
throw new Error("Invalid path. Length should be <= 10") | ||
} | ||
let buf = Buffer.alloc(1 + 4 * path.length); | ||
buf.writeUInt8(path.length, 0); | ||
for (let i = 0; i < path.length; i++) { | ||
let v = path[i]; | ||
if (i < 3) { | ||
v |= 0x80000000; // Harden | ||
} | ||
buf.writeInt32LE(v, 1 + i * 4); | ||
} | ||
return buf; | ||
} | ||
function compressPublicKey(publicKey) { | ||
if (publicKey.length !== 65) { | ||
throw new Error('decompressed public key length should be 65 bytes'); | ||
} | ||
const y = publicKey.slice(33, 65); | ||
const z = new Buffer.from([2 + (y[y.length - 1] & 1)]); | ||
return Buffer.concat([z, publicKey.slice(1, 33)]); | ||
} | ||
export function signGetChunks(path, message) { | ||
let chunks = []; | ||
chunks.push(serializePath(path)); | ||
let buffer = Buffer.from(message); | ||
for (let i = 0; i < buffer.length; i += CHUNK_SIZE) { | ||
let end = i + CHUNK_SIZE; | ||
if (i > buffer.length) { | ||
end = buffer.length; | ||
} | ||
chunks.push(buffer.slice(i, end)); | ||
} | ||
return chunks; | ||
} | ||
export default class CosmosApp { | ||
constructor(transport, scrambleKey = 'CSM') { | ||
if (typeof transport == 'undefined') { | ||
throw new Error('Transport has not been defined'); | ||
} | ||
this.transport = transport; | ||
transport.decorateAppAPIMethods( | ||
this, | ||
[ | ||
'getVersion', | ||
'publicKey', | ||
'sign', | ||
'getAddressAndPubKey', | ||
'appInfo', | ||
'deviceInfo', | ||
'getBech32FromPK' | ||
], | ||
scrambleKey, | ||
); | ||
} | ||
async getVersion() { | ||
return this.transport.send(CLA, INS.GET_VERSION, 0, 0) | ||
.then( | ||
(response) => { | ||
const errorCodeData = response.slice(-2); | ||
const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; | ||
return { | ||
return_code: returnCode, | ||
error_message: errorCodeToString(returnCode), | ||
///// | ||
test_mode: response[0] !== 0, | ||
major: response[1], | ||
minor: response[2], | ||
patch: response[3], | ||
device_locked: response[4] === 1 | ||
}; | ||
}, | ||
processErrorResponse | ||
); | ||
} | ||
async appInfo() { | ||
return this.transport.send(0xB0, 0x01, 0, 0) | ||
.then( | ||
(response) => { | ||
const errorCodeData = response.slice(-2); | ||
const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; | ||
let appName = "err"; | ||
let appVersion = "err"; | ||
let flagLen = 0; | ||
let flagsValue = 0; | ||
if (response[0] !== 1) { | ||
// Ledger responds with format ID 1. There is no spec for any format != 1 | ||
result["error_message"] = "response format ID not recognized"; | ||
result.return_code = 0x9001; | ||
} else { | ||
const appNameLen = response[1]; | ||
appName = response.slice(2, 2 + appNameLen).toString('ascii'); | ||
var idx = 2 + appNameLen; | ||
const appVersionLen = response[idx]; | ||
idx++; | ||
appVersion = response.slice(idx, idx + appVersionLen).toString('ascii'); | ||
idx += appVersionLen; | ||
const appFlagsLen = response[idx]; | ||
idx++; | ||
flagLen = appFlagsLen; | ||
flagsValue = response[idx]; | ||
} | ||
return { | ||
return_code: returnCode, | ||
error_message: errorCodeToString(returnCode), | ||
//// | ||
appName: appName, | ||
appVersion: appVersion, | ||
flagLen: flagLen, | ||
flagsValue: flagsValue, | ||
flag_recovery: (flagsValue & 1) !== 0, | ||
flag_signed_mcu_code: (flagsValue & 2) !== 0, | ||
flag_onboarded: (flagsValue & 4) !== 0, | ||
flag_pin_validated: (flagsValue & 128) !== 0 | ||
}; | ||
}, | ||
processErrorResponse | ||
); | ||
} | ||
async deviceInfo() { | ||
return this.transport.send(0xE0, 0x01, 0, 0, Buffer.from([]), [0x9000, 0x6E00]) | ||
.then( | ||
(response) => { | ||
const errorCodeData = response.slice(-2); | ||
const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; | ||
if (returnCode === 0x6E00) { | ||
return { | ||
return_code: returnCode, | ||
error_message: "This command is only available in the Dashboard", | ||
} | ||
} | ||
const targetId = response.slice(0, 4).toString('hex'); | ||
let pos = 4; | ||
const secureElementVersionLen = response[pos++]; | ||
const seVersion = response.slice(pos, pos + secureElementVersionLen).toString(); | ||
pos += secureElementVersionLen; | ||
const flagsLen = response[pos++]; | ||
const flag = response.slice(pos, pos + flagsLen).toString('hex'); | ||
pos += flagsLen; | ||
const mcuVersionLen = response[pos++]; | ||
// Patch issue in mcu version | ||
let tmp = response.slice(pos, pos + mcuVersionLen); | ||
if (tmp[mcuVersionLen - 1] === 0) { | ||
tmp = response.slice(pos, pos + mcuVersionLen - 1); | ||
} | ||
const mcuVersion = tmp.toString(); | ||
return { | ||
return_code: returnCode, | ||
error_message: errorCodeToString(returnCode), | ||
//// | ||
targetId: targetId, | ||
seVersion: seVersion, | ||
flag: flag, | ||
mcuVersion: mcuVersion, | ||
}; | ||
} | ||
, | ||
processErrorResponse | ||
); | ||
} | ||
async publicKey(path) { | ||
path = serializePath(path); | ||
return this.transport.send(CLA, INS.PUBLIC_KEY_SECP256K1, 0, 0, path) | ||
.then( | ||
(response) => { | ||
const errorCodeData = response.slice(-2); | ||
const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; | ||
let pk = Buffer.from(response.slice(0, 65)); | ||
return { | ||
pk: pk, | ||
compressed_pk: compressPublicKey(pk), | ||
return_code: returnCode, | ||
error_message: errorCodeToString(returnCode) | ||
}; | ||
}, | ||
processErrorResponse | ||
); | ||
}; | ||
async getAddressAndPubKey(path, hrp) { | ||
let data = Buffer.concat([serializeHRP(hrp), serializePath(path)]); | ||
return this.transport.send(CLA, INS.GET_ADDR_SECP256K1, 0, 0, data, [0x9000]) | ||
.then( | ||
(response) => { | ||
const errorCodeData = response.slice(-2); | ||
const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; | ||
let compressedPk = Buffer.from(response.slice(0, 33)); | ||
let bech32Address = Buffer.from(response.slice(33, -2)).toString(); | ||
let pk = Buffer.from(response.slice(0, 65)); | ||
return { | ||
bech32_address: bech32Address, | ||
compressed_pk: compressedPk, | ||
return_code: returnCode, | ||
error_message: errorCodeToString(returnCode) | ||
}; | ||
}, | ||
processErrorResponse | ||
); | ||
}; | ||
async sign_send_chunk(chunk_idx, chunk_num, chunk) { | ||
return this.transport.send(CLA, INS.SIGN_SECP256K1, chunk_idx, chunk_num, chunk, [0x9000, 0x6A80]) | ||
.then( | ||
(response) => { | ||
const errorCodeData = response.slice(-2); | ||
const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; | ||
let errorMessage = errorCodeToString(returnCode); | ||
if (returnCode === 0x6A80) { | ||
errorMessage = response.slice(0, response.length - 2).toString('ascii'); | ||
} | ||
let signature = null; | ||
if (response.length > 2) { | ||
signature = response.slice(0, response.length - 2); | ||
} | ||
return { | ||
signature: signature, | ||
return_code: returnCode, | ||
error_message: errorMessage | ||
}; | ||
}, | ||
processErrorResponse | ||
); | ||
}; | ||
async sign(path, message) { | ||
let chunks = signGetChunks(path, message); | ||
return this.sign_send_chunk(1, chunks.length, chunks[0], [0x9000]) | ||
.then( | ||
async (result) => { | ||
for (let i = 1; i < chunks.length; i++) { | ||
result = await this.sign_send_chunk(1 + i, chunks.length, chunks[i]); | ||
if (result.return_code !== 0x9000) { | ||
break; | ||
} | ||
} | ||
return { | ||
return_code: result.return_code, | ||
error_message: result.error_message, | ||
///// | ||
signature: result.signature, | ||
}; | ||
}, | ||
processErrorResponse | ||
); | ||
}; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
296110
0
17
14
1149
12
2
+ Added@babel/runtime@^7.4.4
+ Added@babel/runtime@7.25.6(transitive)
+ Added@ledgerhq/devices@4.78.0(transitive)
+ Added@ledgerhq/errors@4.78.0(transitive)
+ Added@ledgerhq/hw-transport@4.78.0(transitive)
+ Added@ledgerhq/logs@4.72.0(transitive)
+ Addedevents@3.3.0(transitive)
+ Addedregenerator-runtime@0.14.1(transitive)
+ Addedrxjs@6.6.7(transitive)
+ Addedstring_decoder@1.3.0(transitive)
+ Addedtslib@1.14.1(transitive)
- Removednode-hid@0.7.4
- Removedq@^1.5.1
- Removedansi-regex@2.1.1(transitive)
- Removedaproba@1.2.0(transitive)
- Removedare-we-there-yet@1.1.7(transitive)
- Removedbase64-js@1.5.1(transitive)
- Removedbindings@1.5.0(transitive)
- Removedbl@4.1.0(transitive)
- Removedbuffer@5.7.1(transitive)
- Removedchownr@1.1.4(transitive)
- Removedcode-point-at@1.1.0(transitive)
- Removedconsole-control-strings@1.1.0(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removeddecompress-response@4.2.1(transitive)
- Removeddeep-extend@0.6.0(transitive)
- Removeddelegates@1.0.0(transitive)
- Removeddetect-libc@1.0.3(transitive)
- Removedend-of-stream@1.4.4(transitive)
- Removedexpand-template@2.0.3(transitive)
- Removedfile-uri-to-path@1.0.0(transitive)
- Removedfs-constants@1.0.0(transitive)
- Removedgauge@2.7.4(transitive)
- Removedgithub-from-package@0.0.0(transitive)
- Removedhas-unicode@2.0.1(transitive)
- Removedieee754@1.2.1(transitive)
- Removedini@1.3.8(transitive)
- Removedis-fullwidth-code-point@1.0.0(transitive)
- Removedisarray@1.0.0(transitive)
- Removedmimic-response@2.1.0(transitive)
- Removedminimist@1.2.8(transitive)
- Removedmkdirp-classic@0.5.3(transitive)
- Removednan@2.20.0(transitive)
- Removednapi-build-utils@1.0.2(transitive)
- Removednode-abi@2.30.1(transitive)
- Removednode-hid@0.7.4(transitive)
- Removednoop-logger@0.1.1(transitive)
- Removednpmlog@4.1.2(transitive)
- Removednumber-is-nan@1.0.1(transitive)
- Removedobject-assign@4.1.1(transitive)
- Removedonce@1.4.0(transitive)
- Removedprebuild-install@5.3.6(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedpump@3.0.2(transitive)
- Removedq@1.5.1(transitive)
- Removedrc@1.2.8(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedsafe-buffer@5.1.2(transitive)
- Removedsemver@5.7.2(transitive)
- Removedset-blocking@2.0.0(transitive)
- Removedsignal-exit@3.0.7(transitive)
- Removedsimple-concat@1.0.1(transitive)
- Removedsimple-get@3.1.1(transitive)
- Removedstring-width@1.0.2(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedstrip-ansi@3.0.1(transitive)
- Removedstrip-json-comments@2.0.1(transitive)
- Removedtar-fs@2.1.1(transitive)
- Removedtar-stream@2.2.0(transitive)
- Removedtunnel-agent@0.6.0(transitive)
- Removedwhich-pm-runs@1.1.0(transitive)
- Removedwide-align@1.1.5(transitive)
- Removedwrappy@1.0.2(transitive)