@zondax/ledger-filecoin
Version:
Node API for Filecoin App (Ledger Nano S/X)
317 lines (266 loc) • 9.41 kB
JavaScript
/** ******************************************************************************
* (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.
******************************************************************************* */
import Eth from "@ledgerhq/hw-app-eth";
import { serializePathv1, signSendChunkv1 } from "./helperV1";
import {
APP_KEY,
CHUNK_SIZE,
CLA,
ERROR_CODE,
errorCodeToString,
getVersion,
INS,
P1_VALUES,
PKLEN,
processErrorResponse,
} from "./common";
const varint = require("varint");
function processGetAddrResponse(response) {
let partialResponse = response;
const errorCodeData = partialResponse.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
const pk = Buffer.from(partialResponse.slice(0, PKLEN));
partialResponse = partialResponse.slice(PKLEN);
const addrByteLength = partialResponse[0];
partialResponse = partialResponse.slice(1);
const addrByte = Buffer.from(partialResponse.slice(0, addrByteLength));
partialResponse = partialResponse.slice(addrByteLength);
const addrStringLength = partialResponse[0];
partialResponse = partialResponse.slice(1);
const addrString = Buffer.from(partialResponse.slice(0, addrStringLength)).toString();
return {
addrByte,
addrString,
compressed_pk: pk,
return_code: returnCode,
error_message: errorCodeToString(returnCode),
};
}
export default class FilecoinApp {
constructor(transport, scrambleKey = APP_KEY, ethScrambleKey = "w0w", ethLoadConfig = {}) {
if (!transport) {
throw new Error("Transport has not been defined");
}
this.transport = transport;
this.eth = new Eth(transport, ethScrambleKey, ethLoadConfig);
transport.decorateAppAPIMethods(
this,
["getVersion", "appInfo", "deviceInfo", "getAddressAndPubKey", "sign"],
scrambleKey,
);
}
static prepareChunks(serializedPathBuffer, message) {
const chunks = [];
// First chunk (only path)
chunks.push(serializedPathBuffer);
const messageBuffer = Buffer.from(message);
const buffer = Buffer.concat([messageBuffer]);
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;
}
async serializePath(path) {
this.versionResponse = await getVersion(this.transport);
if (this.versionResponse.return_code !== ERROR_CODE.NoError) {
throw this.versionResponse;
}
switch (this.versionResponse.major) {
case 0:
case 1:
case 2:
return serializePathv1(path);
default:
return {
return_code: 0x6400,
error_message: "App Version is not supported",
};
}
}
async signGetChunks(path, message) {
const serializedPath = await this.serializePath(path);
return FilecoinApp.prepareChunks(serializedPath, message);
}
async getVersion() {
return getVersion(this.transport)
.then((response) => {
this.versionResponse = response;
return response;
})
.catch((err) => processErrorResponse(err));
}
async appInfo() {
return this.transport.send(0xb0, 0x01, 0, 0).then((response) => {
const errorCodeData = response.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
const result = {};
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");
let idx = 2 + appNameLen;
const appVersionLen = response[idx];
idx += 1;
appVersion = response.slice(idx, idx + appVersionLen).toString("ascii");
idx += appVersionLen;
const appFlagsLen = response[idx];
idx += 1;
flagLen = appFlagsLen;
flagsValue = response[idx];
}
return {
return_code: returnCode,
error_message: errorCodeToString(returnCode),
// //
appName,
appVersion,
flagLen,
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([]), [ERROR_CODE.NoError, 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];
pos += 1;
const seVersion = response.slice(pos, pos + secureElementVersionLen).toString();
pos += secureElementVersionLen;
const flagsLen = response[pos];
pos += 1;
const flag = response.slice(pos, pos + flagsLen).toString("hex");
pos += flagsLen;
const mcuVersionLen = response[pos];
pos += 1;
// 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,
seVersion,
flag,
mcuVersion,
};
}, processErrorResponse);
}
async getAddressAndPubKey(path) {
return this.serializePath(path)
.then((data) => {
return this.transport
.send(CLA, INS.GET_ADDR_SECP256K1, P1_VALUES.ONLY_RETRIEVE, 0, data, [0x9000])
.then(processGetAddrResponse, processErrorResponse);
})
.catch((err) => processErrorResponse(err));
}
async showAddressAndPubKey(path) {
return this.serializePath(path)
.then((data) => {
return this.transport
.send(CLA, INS.GET_ADDR_SECP256K1, P1_VALUES.SHOW_ADDRESS_IN_DEVICE, 0, data, [0x9000])
.then(processGetAddrResponse, processErrorResponse);
})
.catch((err) => processErrorResponse(err));
}
async signSendChunk(chunkIdx, chunkNum, chunk, ins) {
switch (this.versionResponse.major) {
case 0:
case 1:
case 2:
return signSendChunkv1(this, chunkIdx, chunkNum, chunk, ins);
default:
return {
return_code: 0x6400,
error_message: "App Version is not supported",
};
}
}
async signGeneric(path, message, ins) {
return this.signGetChunks(path, message).then((chunks) => {
return this.signSendChunk(1, chunks.length, chunks[0], ins, [ERROR_CODE.NoError]).then(
async (response) => {
let result = {
return_code: response.return_code,
error_message: response.error_message,
signature_compact: null,
signature_der: null,
};
for (let i = 1; i < chunks.length; i += 1) {
result = await this.signSendChunk(1 + i, chunks.length, chunks[i], ins);
if (result.return_code !== ERROR_CODE.NoError) {
break;
}
}
return {
return_code: result.return_code,
error_message: result.error_message,
// ///
signature_compact: result.signature_compact,
signature_der: result.signature_der,
};
},
processErrorResponse,
);
}, processErrorResponse);
}
async sign(path, message) {
return this.signGeneric(path, message, INS.SIGN_SECP256K1);
}
async signRawBytes(path, message) {
const msg = Buffer.from(message);
const len = Buffer.from(varint.encode(msg.length));
const data = Buffer.concat([len, message]);
return this.signGeneric(path, data, INS.SIGN_RAW_BYTES);
}
async signETHTransaction(path, rawTxHex, resolution = null) {
return this.eth.signTransaction(path, rawTxHex, resolution);
}
async getETHAddress(path, boolDisplay = false, boolChaincode = false) {
return this.eth.getAddress(path, boolDisplay, boolChaincode);
}
}