@thorchain/ledger-thorchain
Version:
Node API for THORChain App (Ledger Nano S/X)
223 lines (197 loc) • 7.33 kB
text/typescript
import type Transport from "@ledgerhq/hw-transport";
import {
CLA,
errorCodeToString,
INS,
LedgerError,
LedgerErrorType,
PAYLOAD_TYPE,
ledgerErrorFromResponse,
} from "./common";
import crypto from "crypto";
import Ripemd160 from "ripemd160";
import { bech32 } from "bech32";
import { PubKeyResponse, SignResponse, VersionResponse } from "./types";
export function serializeHRP(hrp: string): Buffer {
if (!hrp || hrp.length < 3 || hrp.length > 83) {
throw new LedgerError(LedgerErrorType.HPRInvalid);
}
const buf = Buffer.alloc(1 + hrp.length);
buf.writeUInt8(hrp.length, 0);
buf.write(hrp, 1);
return buf;
}
export function getBech32FromPK(hrp: string, pk: Buffer): string {
if (pk.length !== 33) {
throw new LedgerError(LedgerErrorType.PKInvalidBytes);
}
const hashSha256 = crypto.createHash("sha256").update(pk).digest();
const hashRip = new Ripemd160().update(hashSha256).digest();
const words = bech32.toWords(hashRip);
return bech32.encode(hrp, words);
}
export function serializePath(path: number[]): Buffer {
if (!path || path.length !== 5) {
throw new Error("Invalid path.");
}
const buf = Buffer.alloc(20);
buf.writeUInt32LE(0x80000000 + path[0], 0);
buf.writeUInt32LE(0x80000000 + path[1], 4);
buf.writeUInt32LE(0x80000000 + path[2], 8);
buf.writeUInt32LE(path[3], 12);
buf.writeUInt32LE(path[4], 16);
return buf;
}
export async function signSendChunk(
transport: Transport,
chunkIdx: number,
chunkNum: number,
chunk: Buffer,
): Promise<SignResponse> {
let payloadType = PAYLOAD_TYPE.ADD;
if (chunkIdx === 1) {
payloadType = PAYLOAD_TYPE.INIT;
}
if (chunkIdx === chunkNum) {
payloadType = PAYLOAD_TYPE.LAST;
}
const response: Buffer = await transport.send(CLA, INS.SIGN_SECP256K1, payloadType, 0, chunk, [
LedgerErrorType.NoErrors,
LedgerErrorType.DataIsInvalid,
LedgerErrorType.BadKeyHandle,
LedgerErrorType.SignVerifyError,
]);
const errorCodeData = response.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
let errorMessage = errorCodeToString(returnCode);
if (
returnCode === LedgerErrorType.BadKeyHandle ||
returnCode === LedgerErrorType.DataIsInvalid ||
returnCode === LedgerErrorType.SignVerifyError
) {
errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`;
throw new LedgerError(returnCode, errorMessage);
}
if (returnCode === LedgerErrorType.NoErrors && response.length > 2) {
return {
returnCode,
errorMessage,
signature: response.slice(0, response.length - 2),
};
}
return {
returnCode,
errorMessage,
};
}
export async function getVersion(transport: Transport): Promise<VersionResponse> {
try {
const response: Buffer = await transport.send(CLA, INS.GET_VERSION, 0, 0);
const errorCodeData = response.slice(-2);
const returnCode = (errorCodeData[0] * 256 + errorCodeData[1]) as LedgerErrorType;
let targetId = 0;
if (response.length >= 9) {
/* eslint-disable no-bitwise */
targetId = (response[5] << 24) + (response[6] << 16) + (response[7] << 8) + (response[8] << 0);
/* eslint-enable no-bitwise */
}
return {
returnCode,
errorMessage: errorCodeToString(returnCode),
testMode: response[0] !== 0,
major: response[1],
minor: response[2],
patch: response[3],
deviceLocked: response[4] === 1,
targetId: targetId.toString(16),
};
} catch (error) {
throw ledgerErrorFromResponse(error);
}
}
export async function getPublicKey(transport: Transport, data: Buffer): Promise<PubKeyResponse> {
try {
const response = await transport.send(CLA, INS.GET_ADDR_SECP256K1, 0, 0, data, [
LedgerErrorType.NoErrors,
]);
const errorCodeData = response.slice(-2);
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
const compressedPk = Buffer.from(response.slice(0, 33));
return {
compressedPk,
returnCode,
errorMessage: errorCodeToString(returnCode),
};
} catch (error) {
throw ledgerErrorFromResponse(error);
}
}
/**
* Takes raw output from Ledger device which is TLV encoded in the format "0x30 L 0x02 Lr r 0x02 Ls s"
* where L is length of entire message (after 2nd byte), Lr is length of r and Ls is length of s.
* This function extracts r and s, ensures they are 32 bytes each, joins into a single 64 byte Array.
* This value returned should be base64 encoded to a string to add to the JSON 'signatures' to send to RPC.
*
* Example raw sig:
* [ 48, 69, 2, 33,
* 0, 161, 207, 217, 127, 151, 190, 23, 98, 217, 186, 108, 82, 102, 19, 222, 20, 171, 6, 240, 134, 195, 251, 98, 190, 246, 228, 243, 215, 95, 166, 121, 165,
* 2, 32,
* 69, 188, 53, 213, 24, 9, 191, 90, 244, 21, 213, 146, 240, 109, 156, 221, 247, 63, 131, 52, 150, 253, 199, 153, 132, 76, 91, 239, 28, 254, 68, 80 ]
*
* 48 69 -- length is 69
* 2 33 -- length of r is 33 bytes
* <33 bytes of r> -- (the leading zero here can be dropped)
* 2 32 -- length of s is 32 bytes
* <32 bytes of s>
*/
export function extractSignatureFromTLV(signatureArray: Buffer): Buffer {
// Check Type Length Value encoding
if (signatureArray.length < 64) {
throw Error("Invalid Signature: Too short");
}
if (signatureArray[0] !== 0x30) {
throw Error("Invalid Ledger Signature TLV encoding: expected first byte 0x30");
}
if (signatureArray[1] + 2 !== signatureArray.length) {
throw Error("Invalid Signature: signature length does not match TLV");
}
if (signatureArray[2] !== 0x02) {
throw Error("Invalid Ledger Signature TLV encoding: expected length type 0x02");
}
// r signature
const rLength = signatureArray[3];
let rSignature = signatureArray.slice(4, rLength + 4);
// Drop leading zero on some 'r' signatures that are 33 bytes.
if (rSignature.length === 33 && rSignature[0] === 0) {
rSignature = rSignature.slice(1, 33);
} else if (rSignature.length === 33) {
throw Error('Invalid signature: "r" too long');
}
// add leading zero's to pad to 32 bytes
while (rSignature.length < 32) {
Buffer.concat([Buffer.from([0]), rSignature]);
}
// s signature
if (signatureArray[rLength + 4] !== 0x02) {
throw Error("Invalid Ledger Signature TLV encoding: expected length type 0x02");
}
const sLength = signatureArray[rLength + 5];
if (4 + rLength + 2 + sLength != signatureArray.length) {
throw Error("Invalid Ledger Signature: TLV byte lengths do not match message length");
}
let sSignature = signatureArray.slice(rLength + 6, signatureArray.length);
// Drop leading zero on 's' signatures that are 33 bytes. This shouldn't occur since ledger signs using "Small s" math. But just to be sure...
if (sSignature.length === 33 && sSignature[0] === 0) {
sSignature = sSignature.slice(1, 33);
} else if (sSignature.length === 33) {
throw Error('Invalid signature: "s" too long');
}
// add leading zero's to pad to 32 bytes
while (sSignature.length < 32) {
Buffer.concat([Buffer.from([0]), sSignature]);
}
if (rSignature.length !== 32 || sSignature.length !== 32) {
throw Error("Invalid signatures: must be 32 bytes each");
}
return Buffer.concat([rSignature, sSignature])
}