UNPKG

@thorchain/ledger-thorchain

Version:
223 lines (197 loc) 7.33 kB
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]) }