/********************************************************************************
 *   Binance Chain Ledger App Interface
 *   (c) 2018-2019 Binance
 *   (c) 2018 ZondaX GmbH
 *
 *  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 Transport from "@ledgerhq/hw-transport" ; const DEFAULT_LEDGER_INTERACTIVE_TIMEOUT = 50000 ; const DEFAULT_LEDGER_NONINTERACTIVE_TIMEOUT = 3000 ; const CLA = 0xbc ; const SCRAMBLE_KEY = "CSM" ; const ACCEPT_STATUSES = [ 0x9000 ] ; const CHUNK_SIZE = 250 ; const INS_GET_VERSION = 0x00 ; const INS_PUBLIC_KEY_SECP256K1 = 0x01 ; const INS_SIGN_SECP256K1 = 0x02 ; const INS_SHOW_ADDR_SECP256K1 = 0x03 ; export interface Version { test_mode ? : boolean ; major ? : number ; minor ? : number ; patch ? : number ; device_locked ? : boolean ; return_code ? : number ; error_message ? : string ; } export interface PublicKey { pk ? : Buffer ; return_code ? : number ; error_message ? : string ; } export interface SignedSignature { return_code ? : number ; error_message ? : string ; signature ? : Buffer | null ; } export interface ReturnResponse { return_code ? : number ; error_message ? : string ; } static class LedgerApp { private _transport : Transport ; private _interactiveTimeout : number ; private _nonInteractiveTimeout : number ; param { Transport } transport param { Number } interactiveTimeout param { Number } nonInteractiveTimeout constructor ( transport , interactiveTimeout = DEFAULT_LEDGER_INTERACTIVE_TIMEOUT , nonInteractiveTimeout = DEFAULT_LEDGER_NONINTERACTIVE_TIMEOUT ) { if ( ! transport || ! transport . send ) { throw new Error ( "LedgerApp expected a Transport" ) ; } this . _transport = transport ; this . _interactiveTimeout = interactiveTimeout ; this . _nonInteractiveTimeout = nonInteractiveTimeout ; this . _transport . setScrambleKey ( SCRAMBLE_KEY ) ; } _serialize ( cla = CLA , ins , p1 = 0 , p2 = 0 , data = null ) { let size = 5 ; if ( data != null ) { if ( data . length > 255 ) { throw new Error ( "maximum data size = 255" ) ; } size += data . length ; } const buffer = Buffer . alloc ( size ) ; buffer [ 0 ] = cla ; buffer [ 1 ] = ins ; buffer [ 2 ] = p1 ; buffer [ 3 ] = p2 ; buffer [ 4 ] = 0 ; if ( data != null ) { buffer [ 4 ] = data . length ; buffer . set ( data , 5 ) ; } return buffer ; } _serializeHRP ( hrp ) { if ( hrp == null || hrp . length < 3 || hrp . length > 83 ) { throw new Error ( "Invalid HRP" ) ; } const buf = Buffer . alloc ( 1 + hrp . length ) ; buf . writeUInt8 ( hrp . length , 0 ) ; buf . write ( hrp , 1 ) ; return buf ; } _serializeHDPath ( 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" ) ; } const 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 ; } buf . writeInt32LE ( v , 1 + i * 4 ) ; } return buf ; } _errorMessage ( code ) { switch ( code ) { case 1 : return "U2F: Unknown" ; case 2 : return "U2F: Bad request" ; case 3 : return "U2F: Configuration unsupported" ; case 4 : return "U2F: Device Ineligible" ; case 5 : return "U2F: Timeout" ; case 14 : return "Timeout" ; case 0x9000 : return "No errors" ; case 0x9001 : return "Device is busy" ; case 0x6400 : return "Execution Error" ; case 0x6700 : return "Wrong Length" ; case 0x6982 : return "Empty Buffer" ; case 0x6983 : return "Output buffer too small" ; case 0x6984 : return "Data is invalid" ; case 0x6985 : return "Conditions not satisfied" ; case 0x6986 : return "Transaction rejected" ; case 0x6a80 : return "Bad key handle" ; case 0x6b00 : return "Invalid P1/P2" ; case 0x6d00 : return "Instruction not supported" ; case 0x6e00 : return "The app does not seem to be open" ; case 0x6f00 : return "Unknown error" ; case 0x6f01 : return "Sign/verify error" ; default : return "Unknown error code" ; } } throws async getVersion ( ) { const result = { } ; try { this . _transport . setExchangeTimeout ( this . _nonInteractiveTimeout ) ; const apduResponse = await this . _transport . send ( CLA , INS_GET_VERSION , 0 , 0 , Buffer . alloc ( 0 ) , ACCEPT_STATUSES ) ; if ( ! Buffer . isBuffer ( apduResponse ) ) throw new Error ( "expected apduResponse to be Buffer" ) ; const returnCode = apduResponse . slice ( - 2 ) ; result [ "test_mode" ] = apduResponse [ 0 ] !== 0 ; result [ "major" ] = apduResponse [ 1 ] ; result [ "minor" ] = apduResponse [ 2 ] ; result [ "patch" ] = apduResponse [ 3 ] ; result [ "device_locked" ] = apduResponse [ 4 ] === 1 ; result [ "return_code" ] = returnCode [ 0 ] * 256 + returnCode [ 1 ] ; result [ "error_message" ] = this . _errorMessage ( result [ "return_code" ] ) ; } catch ( err ) { const { statusCode , statusText , message , stack } = err ; console . warn ( "Ledger getVersion error:" , this . _errorMessage ( statusCode ) , message , statusText , stack ) ; throw err ; } return result ; } param { array } hdPath throws async publicKeySecp256k1 ( hdPath = [ 44 , 714 , 0 , 0 , 0 ] ) { const result = { } ; try { this . _transport . setExchangeTimeout ( this . _nonInteractiveTimeout ) ; const apduResponse = await this . _transport . send ( CLA , INS_PUBLIC_KEY_SECP256K1 , 0 , 0 , this . _serializeHDPath ( hdPath ) , ACCEPT_STATUSES ) ; if ( ! Buffer . isBuffer ( apduResponse ) ) throw new Error ( "expected apduResponse to be Buffer" ) ; const returnCode = apduResponse . slice ( - 2 ) ; result [ "pk" ] = apduResponse . slice ( 0 , 1 + 64 ) ; result [ "return_code" ] = returnCode [ 0 ] * 256 + returnCode [ 1 ] ; result [ "error_message" ] = this . _errorMessage ( result [ "return_code" ] ) ; } catch ( err ) { const { statusCode , statusText , message , stack } = err ; console . warn ( "Ledger publicKeySecp256k1 error:" , this . _errorMessage ( statusCode ) , message , statusText , stack ) ; throw err ; } return result ; } _signGetChunks ( data , hdPath ) { const chunks = [ ] ; chunks . push ( this . _serializeHDPath ( hdPath ) ) ; const buffer = Buffer . from ( data ) ; 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 _signSendChunk ( chunkIdx , chunksCount , chunk ) { const result = { } ; try { const apduResponse = await this . _transport . send ( CLA , INS_SIGN_SECP256K1 , chunkIdx , chunksCount , chunk ) ; if ( ! Buffer . isBuffer ( apduResponse ) ) throw new Error ( "expected apduResponse to be Buffer" ) ; const returnCode = apduResponse . slice ( - 2 ) ; result [ "return_code" ] = returnCode [ 0 ] * 256 + returnCode [ 1 ] ; result [ "error_message" ] = this . _errorMessage ( result [ "return_code" ] ) ; result [ "signature" ] = null ; if ( apduResponse . length > 2 ) { result [ "signature" ] = apduResponse . slice ( 0 , apduResponse . length - 2 ) ; } } catch ( err ) { const { statusCode , statusText , message , stack } = err ; console . warn ( "Ledger signSendChunk error:" , this . _errorMessage ( statusCode ) , message , statusText , stack ) ; throw err ; } return result ; } param { Buffer } signBytes param { array } hdPath throws async signSecp256k1 ( signBytes , hdPath = [ 44 , 714 , 0 , 0 , 0 ] ) { const result = { } ; const chunks = this . _signGetChunks ( signBytes , hdPath ) ; let response ; try { if ( chunks . length <= 1 ) { this . _transport . setExchangeTimeout ( this . _interactiveTimeout ) ; } else { this . _transport . setExchangeTimeout ( this . _nonInteractiveTimeout ) ; } response = await this . _signSendChunk ( 1 , chunks . length , chunks [ 0 ] ) ; result [ "return_code" ] = response . return_code ; result [ "error_message" ] = response . error_message ; result [ "signature" ] = null ; } catch ( err ) { const { statusCode , statusText , message , stack } = err ; console . warn ( "Ledger signSecp256k1 error (chunk 1):" , this . _errorMessage ( statusCode ) , message , statusText , stack ) ; throw err ; } if ( response . return_code === 0x9000 ) { for ( let i = 1 ; i < chunks . length ; i ++ ) { try { if ( i === chunks . length - 1 ) { this . _transport . setExchangeTimeout ( this . _interactiveTimeout ) ; } response = await this . _signSendChunk ( 1 + i , chunks . length , chunks [ i ] ) ; result [ "return_code" ] = response . return_code ; result [ "error_message" ] = response . error_message ; } catch ( err ) { const { statusCode , statusText , message , stack } = err ; console . warn ( "Ledger signSecp256k1 error (chunk 2):" , this . _errorMessage ( statusCode ) , message , statusText , stack ) ; throw err ; } if ( response . return_code !== 0x9000 ) { break ; } } result [ "return_code" ] = response . return_code ; result [ "error_message" ] = response . error_message ; let signature = response . signature ; if ( ! signature || ! signature . length ) { throw new Error ( "Ledger assertion failed: Expected a non-empty signature from the device" ) ; } if ( signature [ 0 ] !== 0x30 ) { throw new Error ( "Ledger assertion failed: Expected a signature header of 0x30" ) ; } let rOffset = 4 ; let rLen = signature [ 3 ] ; const sLen = signature [ 4 + rLen + 1 ] ; let sOffset = signature . length - sLen ; if ( rLen === 33 ) { rOffset ++ ; rLen -- ; } if ( sLen === 33 ) sOffset ++ ; const sigR = signature . slice ( rOffset , rOffset + rLen ) ; const sigS = signature . slice ( sOffset ) ; signature = result [ "signature" ] = Buffer . concat ( [ sigR , sigS ] ) ; if ( signature . length !== 64 ) { throw new Error ( `Ledger assertion failed: incorrect signature length ${ signature . length }` ) ; } } else { throw new Error ( "Unable to sign the transaction. Return code " + response . return_code ) ; } return result ; } param { string } hrp param { array } hdPath throws async showAddress ( hrp = "bnb" , hdPath = [ 44 , 714 , 0 , 0 , 0 ] ) { const result = { } ; const data = Buffer . concat ( [ this . _serializeHRP ( hrp ) , this . _serializeHDPath ( hdPath ) , ] ) ; this . _transport . setExchangeTimeout ( this . _interactiveTimeout ) ; const apduResponse = await this . _transport . send ( CLA , INS_SHOW_ADDR_SECP256K1 , 0 , 0 , data , ACCEPT_STATUSES ) ; if ( ! Buffer . isBuffer ( apduResponse ) ) throw new Error ( "expected apduResponse to be Buffer" ) ; const returnCode = apduResponse . slice ( - 2 ) ; result [ "return_code" ] = returnCode [ 0 ] * 256 + returnCode [ 1 ] ; result [ "error_message" ] = this . _errorMessage ( result [ "return_code" ] ) ; if ( result . return_code === 0x6a80 ) { result [ "error_message" ] = apduResponse . slice ( 0 , apduResponse . length - 2 ) . toString ( "ascii" ) ; } return result ; } param { array } hdPath throws getPublicKey ( hdPath ) { return this . publicKeySecp256k1 ( hdPath ) ; } param { Buffer } signBytes param { array } hdPath throws sign ( signBytes , hdPath ) { return this . signSecp256k1 ( signBytes , hdPath ) ; } } module . exports = LedgerApp ; export default LedgerApp ;