UNPKG

iso-filecoin

Version:

Isomorphic filecoin abstractions for RPC, signatures, address, token and wallet

425 lines (376 loc) 10.4 kB
import { StatusCodes } from '@ledgerhq/errors' import { secp256k1 as secp } from '@noble/curves/secp256k1' import { blake2b } from '@noble/hashes/blake2b' import { hex } from 'iso-base/rfc4648' import { utf8 } from 'iso-base/utf8' import { buf, concat, u8 } from 'iso-base/utils' import * as varint from 'iso-base/varint' import { AddressSecp256k1 } from './address.js' import { getNetworkFromPath, lotusCid, parseDerivationPath } from './utils.js' /** * @typedef {import('./types.js').Transport} Transport * @typedef {keyof typeof SIGNATURE_TYPE} SignatureType */ /** * Filecoin App APDU return messages * * @see https://github.com/Zondax/ledger-filecoin/blob/main/docs/APDUSPEC.md#return-codes * @type {Record<string, string>} * */ const RETURN_MSGS = { 0x9000: 'Success', 0x9001: 'Busy', 0x6400: 'Execution Error', 0x6700: 'Wrong Length', 0x6982: 'Empty buffer', 0x6983: 'Output buffer too small', 0x6984: 'Data is invalid', 0x6985: 'Conditions not satisfied', 0x6986: 'Command rejected', 0x6a80: 'Bad key handle', 0x6b00: 'Invalid parameter(s)', 0x6d00: 'Instriction not supported', 0x6e00: 'Application not supported', 0x6e01: 'Filecoin app not open', 0x6f00: 'Unknown', 0x6f01: 'Sign/verify error', } /** * APDU codes * * @see https://github.com/tendermint/ledger-validator-app/blob/master/deps/ledger-zxlib/include/apdu_codes.h */ export const APDU_CODES = { // ledger supports OK: 0x9000, BUSY: 0x9001, EXECUTION_ERROR: 0x6400, // ledger supports WRONG_LENGTH: 0x6700, /** * Ledger name is SECURITY_STATUS_NOT_SATISFIED * * @see https://github.com/LedgerHQ/ledger-live/blob/d376d5f165ac2b322f7eb3fceb6106c41e04191b/libs/ledgerjs/packages/errors/src/index.ts#L286 */ EMPTY_BUFFER: 0x6982, OUTPUT_BUFFER_TOO_SMALL: 0x6983, DATA_INVALID: 0x6984, /** * ledger supports * * @see https://github.com/LedgerHQ/ledger-live/blob/d376d5f165ac2b322f7eb3fceb6106c41e04191b/libs/ledgerjs/packages/errors/src/index.ts#L258 */ CONDITIONS_NOT_SATISFIED: 0x6985, COMMAND_NOT_ALLOWED: 0x6986, /** * Ledger name is INCORRECT_DATA * * @see https://github.com/LedgerHQ/ledger-live/blob/d376d5f165ac2b322f7eb3fceb6106c41e04191b/libs/ledgerjs/packages/errors/src/index.ts#L268 */ BAD_KEY_HANDLE: 0x6a80, /** * ledger supports * * @see https://github.com/LedgerHQ/ledger-live/blob/d376d5f165ac2b322f7eb3fceb6106c41e04191b/libs/ledgerjs/packages/errors/src/index.ts#L270 */ INVALIDP1P2: 0x6b00, // ledger supports INS_NOT_SUPPORTED: 0x6d00, // ledger supports CLA_NOT_SUPPORTED: 0x6e00, // ledger supports UNKNOWN: 0x6f00, SIGN_VERIFY_ERROR: 0x6f01, APP_NOT_OPEN: 0x6e01, } export const EIP191_PREFIX = 'Filecoin Sign Bytes:\n' export const IS_HID_SUPPORTED = // @ts-ignore 'navigator' in globalThis && navigator.hid !== undefined const CHUNK_SIZE = 250 /** * @see https://github.com/Zondax/ledger-filecoin/blob/main/docs/APDUSPEC.md */ const CLA = 0x06 const INS = { GET_VERSION: 0x00, GET_ADDR_SECP256K1: 0x01, SIGN_SECP256K1: 0x02, SIGN_DATA_CAP: 0x05, SIGN_CLIENT_DEAL: 0x06, SIGN_RAW_BYTES: 0x07, } const SIGNATURE_TYPE = { SECP256K1: 0x02, DATA_CAP: 0x05, CLIENT_DEAL: 0x06, RAW_BYTES: 0x07, PERSONAL_MESSAGE: 0x08, } /** * Serialize derivation path * * @param {string} path */ function serializeDerivationPath(path) { const pathComponents = parseDerivationPath(path) const bytes = new ArrayBuffer(20) const view = new DataView(bytes) view.setUint32(0, 0x80000000 + pathComponents.purpose, true) view.setUint32(4, 0x80000000 + pathComponents.coinType, true) view.setUint32(8, 0x80000000 + pathComponents.account, true) view.setUint32(12, pathComponents.change, true) view.setUint32(16, pathComponents.addressIndex, true) return buf(view) } /** * Filecoin app error */ export class FilecoinAppError extends Error { name = 'FilecoinAppError' /** @type {number} */ statusCode /** * @param {number} statusCode The error status code coming from a Transport implementation * @param {string} [data] The error message coming from a instruction call */ constructor(statusCode, data) { const statusCodeStr = statusCode.toString(16) const message = `Filecoin App: ${RETURN_MSGS[statusCode]}${data ? ` ${data}` : ''} (0x${statusCodeStr})` super(message) this.statusCode = statusCode } } /** * Check for error returned in the APDU response * * @param {Buffer} output */ function checkError(output) { const errorCodeData = output.subarray(-2) const code = hex.encode(errorCodeData) const intCode = Number.parseInt(code, 16) if ( intCode === APDU_CODES.DATA_INVALID || intCode === APDU_CODES.BAD_KEY_HANDLE ) { return new FilecoinAppError( intCode, output.subarray(0, output.length - 2).toString('ascii') ) } if (intCode !== APDU_CODES.OK) { return new FilecoinAppError(intCode) } } /** * Sign chunk of data * * @param {Transport} transport - Ledger transport * @param {number} index * @param {number} size * @param {Uint8Array} data * @param {number} [instruction=0x02] */ async function signChunk(transport, index, size, data, instruction = 0x02) { let payloadDesc = 0x00 if (index !== 0) { payloadDesc = 0x01 } if (index === size - 1) { payloadDesc = 0x02 } const out = await transport.send( 0x06, instruction, payloadDesc, 0, buf(data), [ StatusCodes.OK, APDU_CODES.BAD_KEY_HANDLE, APDU_CODES.DATA_INVALID, APDU_CODES.APP_NOT_OPEN, APDU_CODES.COMMAND_NOT_ALLOWED, ] ) const err = checkError(out) if (err) { throw err } return out.subarray(0, 65) } /** * Verify raw signature * * @param {Uint8Array} signature * @param {Uint8Array} data * @param {Uint8Array} publicKey * @returns {boolean} * @example * ```ts twoslash * import { verifyRaw } from 'iso-filecoin/ledger' * * const signature = new Uint8Array([1, 2, 3]) * const data = new Uint8Array([4, 5, 6]) * const publicKey = new Uint8Array([7, 8, 9]) * const isValid = verifyRaw(signature, data, publicKey) * // => true * ``` */ export function verifyRaw(signature, data, publicKey) { const prefix = utf8.decode(EIP191_PREFIX) const prefixed = concat([prefix, data]) const cid = lotusCid(prefixed) return secp.verify( signature.subarray(0, 64), blake2b(cid, { dkLen: 32, }), publicKey ) } /** * Ledger Filecoin app client */ export class LedgerFilecoin { /** * * @param {Transport} transport - Ledger transport */ constructor(transport) { this.transport = transport transport.decorateAppAPIMethods( this, ['getVersion', 'getAddress', 'sign'], 'Filecoin' ) } /** * Get the version of the Filecoin app * * @see https://github.com/LedgerHQ/app-filecoin/blob/develop/docs/APDUSPEC.md#get_version * @example * ```ts twoslash * import { LedgerFilecoin } from 'iso-filecoin/ledger' * import TransportWebUSB from '@ledgerhq/hw-transport-webusb' * * const transport = await TransportWebUSB.create() * const ledger = new LedgerFilecoin(transport) * const version = await ledger.getVersion() * // => '1.0.0' * ``` */ async getVersion() { const out = await this.transport.send( CLA, INS.GET_VERSION, 0, 0, undefined, [StatusCodes.OK, APDU_CODES.APP_NOT_OPEN] ) const err = checkError(out) if (err) { throw err } return out.subarray(1, 4).join('.') } /** * Get the secp256k1 address for a given derivation path * * @see https://github.com/LedgerHQ/app-filecoin/blob/develop/docs/APDUSPEC.md#ins_get_addr_secp256k1 * * @param {string} path - Derivation path * @param {boolean} [showOnDevice=false] - Whether to show the address on the device * @returns {Promise<import('./types.js').IAccount>} */ async getAddress(path, showOnDevice = false) { const out = await this.transport.send( CLA, INS.GET_ADDR_SECP256K1, showOnDevice ? 0x01 : 0x00, 0x00, serializeDerivationPath(path), [StatusCodes.OK, APDU_CODES.APP_NOT_OPEN, APDU_CODES.COMMAND_NOT_ALLOWED] ) const err = checkError(out) if (err) { throw err } const publicKey = u8(out.subarray(0, 65)) return { type: 'SECP256K1', address: AddressSecp256k1.fromPublicKey( publicKey, getNetworkFromPath(path) ), publicKey, path, } } /** * Sign a message * * @param {string} path - Derivation path * @param {Uint8Array} message - Message to sign in bytes * @param {SignatureType} [type=SECP256K1] - Signature type */ async sign(path, message, type = 'SECP256K1') { const chunks = [] chunks.push(serializeDerivationPath(path)) for ( let start = 0, end = 1; start < message.length; start += CHUNK_SIZE, end++ ) { chunks.push(message.subarray(start, CHUNK_SIZE * end)) } let result for (let index = 0; index < chunks.length; index++) { result = await signChunk( this.transport, index, chunks.length, chunks[index], SIGNATURE_TYPE[type] ) } if (!result) { throw new Error('Sign failed') } return new Uint8Array(result.buffer, result.byteOffset, result.byteLength) } /** * Sign raw bytes using prefixed message similar to EIP-191 * * @param {string} path - Derivation path * @param {Uint8Array} message - Message to sign */ signRaw(path, message) { const prefix = utf8.decode(EIP191_PREFIX) const prefixed = concat([prefix, message]) const data = concat([varint.encode(prefixed.length)[0], prefixed]) return this.sign(path, data, 'RAW_BYTES') } /** * Sign a message using FRC-102 * * @param {string} path - Derivation path * @param {Uint8Array} message - Message to sign */ personalSign(path, message) { const buf = new Uint8Array(4) const view = new DataView(buf.buffer) view.setUint32(0, message.length, false) const data = concat([buf, message]) return this.sign(path, data, 'PERSONAL_MESSAGE') } /** * Close the transport */ close() { return this.transport.close() } }