Skip to content

Commit 8ee4450

Browse files
authored
improve payload and error management (#20)
1 parent 0ba9489 commit 8ee4450

File tree

9 files changed

+201
-81
lines changed

9 files changed

+201
-81
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
yarn.lock
3+
.idea
34
dist/

src/app.test.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { MockTransport } from '@ledgerhq/hw-transport-mocker'
1717

1818
import BaseApp from './app'
1919
import { LedgerError } from './consts'
20+
import { ResponseError } from './responseError'
2021

2122
describe('BaseApp', () => {
2223
const params = {
@@ -69,7 +70,7 @@ describe('BaseApp', () => {
6970
describe('getVersion', () => {
7071
it('should retrieve version information', async () => {
7172
const responseBuffer = Buffer.concat([
72-
Buffer.from([0, 1, 2, 3]), // Version information
73+
Buffer.from([0, 1, 2, 3, 0]), // Version information
7374
Buffer.from([0x90, 0x00]), // Status code for no errors (0x9000)
7475
])
7576

@@ -82,20 +83,29 @@ describe('BaseApp', () => {
8283
minor: 2,
8384
patch: 3,
8485
deviceLocked: false,
85-
targetId: '0',
86+
targetId: '',
8687
testMode: false,
8788
})
8889
})
8990

91+
it('should handle missing data', async () => {
92+
const responseBuffer = Buffer.concat([
93+
Buffer.from([0, 1, 2, 3]), // Version information
94+
Buffer.from([0x90, 0x00]), // Status code for no errors (0x9000)
95+
])
96+
97+
const transport = new MockTransport(responseBuffer)
98+
const app = new BaseApp(transport, params)
99+
100+
await expect(app.getVersion()).rejects.toEqual(new ResponseError(LedgerError.UnknownError, 'Attempt to read beyond buffer length'))
101+
})
102+
90103
it('should handle errors correctly', async () => {
91104
const transport = new MockTransport(Buffer.alloc(0))
92105
transport.exchange = jest.fn().mockRejectedValue(new Error('Unknown error'))
93106
const app = new BaseApp(transport, params)
94107

95-
await expect(app.getVersion()).rejects.toEqual({
96-
returnCode: LedgerError.UnknownTransportError,
97-
errorMessage: 'Unknown transport error',
98-
})
108+
await expect(app.getVersion()).rejects.toEqual(ResponseError.fromReturnCode(LedgerError.UnknownTransportError))
99109
})
100110
})
101111

@@ -123,10 +133,7 @@ describe('BaseApp', () => {
123133
transport.exchange = jest.fn().mockRejectedValue(new Error('App does not seem to be open'))
124134
const app = new BaseApp(transport, params)
125135

126-
await expect(app.appInfo()).rejects.toEqual({
127-
returnCode: LedgerError.UnknownTransportError,
128-
errorMessage: 'Unknown transport error',
129-
})
136+
await expect(app.appInfo()).rejects.toEqual(ResponseError.fromReturnCode(LedgerError.UnknownTransportError))
130137
})
131138
})
132139

@@ -156,10 +163,7 @@ describe('BaseApp', () => {
156163
transport.exchange = jest.fn().mockRejectedValue(new Error('Device is busy'))
157164

158165
const app = new BaseApp(transport, params)
159-
await expect(app.deviceInfo()).rejects.toEqual({
160-
returnCode: LedgerError.UnknownTransportError,
161-
errorMessage: 'Unknown transport error',
162-
})
166+
await expect(app.deviceInfo()).rejects.toEqual(ResponseError.fromReturnCode(LedgerError.UnknownTransportError))
163167
})
164168
})
165169
})

src/app.ts

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ import type Transport from '@ledgerhq/hw-transport'
1717

1818
import { processErrorResponse, processResponse } from './common'
1919
import { HARDENED, LEDGER_DASHBOARD_CLA, LedgerError, PAYLOAD_TYPE } from './consts'
20+
import { ResponsePayload } from './payload'
21+
import { ResponseError } from './responseError'
2022
import {
2123
type ConstructorParams,
2224
type INSGeneric,
2325
type P1_VALUESGeneric,
2426
type ResponseAppInfo,
2527
type ResponseDeviceInfo,
26-
ResponseError,
2728
type ResponseVersion,
2829
} from './types'
2930

@@ -128,7 +129,7 @@ export default class BaseApp {
128129
* @returns A promise that resolves to the processed response from the device.
129130
* @throws {ResponseError} If the response from the device indicates an error.
130131
*/
131-
async signSendChunk(ins: number, chunkIdx: number, chunkNum: number, chunk: Buffer): Promise<Buffer> {
132+
async signSendChunk(ins: number, chunkIdx: number, chunkNum: number, chunk: Buffer): Promise<ResponsePayload> {
132133
let payloadType = PAYLOAD_TYPE.ADD
133134

134135
if (chunkIdx === 1) {
@@ -146,6 +147,7 @@ export default class BaseApp {
146147

147148
return response
148149
}
150+
149151
/**
150152
* Retrieves the version information from the device.
151153
* @returns A promise that resolves to the version information.
@@ -156,21 +158,27 @@ export default class BaseApp {
156158
const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_VERSION, 0, 0)
157159
const response = processResponse(responseBuffer)
158160

159-
let targetId = 0
161+
const testMode = response.readBytes(1).readUInt8() !== 0
162+
const major = response.readBytes(1).readUInt8()
163+
const minor = response.readBytes(1).readUInt8()
164+
const patch = response.readBytes(1).readUInt8()
165+
166+
const deviceLocked = response.readBytes(1).readUInt8() === 1
160167

161-
if (response.length >= 9) {
162-
targetId = response.readUInt32BE(5)
168+
let targetId = ''
169+
if (response.length() >= 4) {
170+
targetId = response.readBytes(4).readUInt32BE().toString(16)
163171
}
164172

165173
// FIXME: Add support for devices with multibyte version numbers
166174

167175
return {
168-
testMode: response[0] !== 0,
169-
major: response[1],
170-
minor: response[2],
171-
patch: response[3],
172-
deviceLocked: response[4] === 1,
173-
targetId: targetId.toString(16),
176+
testMode,
177+
major,
178+
minor,
179+
patch,
180+
deviceLocked,
181+
targetId,
174182
}
175183
} catch (error) {
176184
throw processErrorResponse(error)
@@ -187,23 +195,23 @@ export default class BaseApp {
187195
const responseBuffer = await this.transport.send(LEDGER_DASHBOARD_CLA, 0x01, 0, 0)
188196
const response = processResponse(responseBuffer)
189197

190-
if (response[0] !== 1) {
198+
const formatId = response.readBytes(1).readUInt8()
199+
200+
if (formatId !== 1) {
191201
throw {
192202
returnCode: 0x9001,
193203
errorMessage: 'Format ID not recognized',
194204
} as ResponseError
195205
}
196206

197-
const appNameLen = response[1]
198-
const appName = response.subarray(2, 2 + appNameLen).toString('ascii')
199-
let idx = 2 + appNameLen
200-
const appVersionLen = response[idx]
201-
idx += 1
202-
const appVersion = response.subarray(idx, idx + appVersionLen).toString('ascii')
203-
idx += appVersionLen
204-
const flagLen = response[idx]
205-
idx += 1
206-
const flagsValue = response[idx]
207+
const appNameLen = response.readBytes(1).readUInt8()
208+
const appName = response.readBytes(appNameLen).toString('ascii')
209+
210+
const appVersionLen = response.readBytes(1).readUInt8()
211+
const appVersion = response.readBytes(appVersionLen).toString('ascii')
212+
213+
const flagLen = response.readBytes(1).readUInt8()
214+
const flagsValue = response.readBytes(flagLen).readUInt8()
207215

208216
return {
209217
appName,
@@ -230,25 +238,22 @@ export default class BaseApp {
230238
const responseBuffer = await this.transport.send(0xe0, 0x01, 0, 0, Buffer.from([]), [LedgerError.NoErrors, 0x6e00])
231239
const response = processResponse(responseBuffer)
232240

233-
const targetId = response.subarray(0, 4).toString('hex')
241+
const targetId = response.readBytes(4).toString('hex')
242+
243+
const secureElementVersionLen = response.readBytes(1).readUInt8()
244+
const seVersion = response.readBytes(secureElementVersionLen).toString()
234245

235-
let pos = 4
236-
const secureElementVersionLen = response[pos]
237-
pos += 1
238-
const seVersion = response.subarray(pos, pos + secureElementVersionLen).toString()
239-
pos += secureElementVersionLen
246+
const flagsLen = response.readBytes(1).readUInt8()
247+
const flag = response.readBytes(flagsLen).toString('hex')
240248

241-
const flagsLen = response[pos]
242-
pos += 1
243-
const flag = response.subarray(pos, pos + flagsLen).toString('hex')
244-
pos += flagsLen
249+
const mcuVersionLen = response.readBytes(1).readUInt8()
250+
let tmp = response.readBytes(mcuVersionLen)
245251

246-
const mcuVersionLen = response[pos]
247-
pos += 1
248252
// Patch issue in mcu version
249-
let tmp = response.subarray(pos, pos + mcuVersionLen)
250-
if (tmp[mcuVersionLen - 1] === 0) {
251-
tmp = response.subarray(pos, pos + mcuVersionLen - 1)
253+
// Find the first zero byte and trim the buffer up to that point
254+
const firstZeroIndex = tmp.indexOf(0)
255+
if (firstZeroIndex !== -1) {
256+
tmp = tmp.subarray(0, firstZeroIndex)
252257
}
253258
const mcuVersion = tmp.toString()
254259

src/common.test.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*****************************************************************************/
1616
import { errorCodeToString, processErrorResponse } from './common'
1717
import { LedgerError } from './consts'
18+
import { ResponseError } from './responseError'
1819

1920
describe('errorCodeToString', () => {
2021
it('should return the correct error message for a known error code', () => {
@@ -33,10 +34,7 @@ describe('errorCodeToString', () => {
3334
describe('processErrorResponse', () => {
3435
it('should return correct response object when statusCode is present', () => {
3536
const response = { statusCode: 0x9000 }
36-
const expectedResponse = {
37-
returnCode: 0x9000,
38-
errorMessage: 'No errors',
39-
}
37+
const expectedResponse = new ResponseError(0x9000, 'No errors')
4038
expect(processErrorResponse(response)).toEqual(expectedResponse)
4139
})
4240

@@ -47,10 +45,7 @@ describe('processErrorResponse', () => {
4745

4846
it('should return a default error response when neither statusCode nor returnCode/errorMessage are present', () => {
4947
const response = { someOtherKey: 123 }
50-
const expectedResponse = {
51-
returnCode: LedgerError.UnknownTransportError,
52-
errorMessage: 'Unknown transport error',
53-
}
48+
const expectedResponse = ResponseError.fromReturnCode(LedgerError.UnknownTransportError)
5449
expect(processErrorResponse(response)).toEqual(expectedResponse)
5550
})
5651
})

src/common.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* limitations under the License.
1515
*****************************************************************************/
1616
import { ERROR_DESCRIPTION_OVERRIDE, LedgerError } from './consts'
17-
import { ResponseError, ResponsePayload, type ResponseReturnCode } from './types'
17+
import { ResponsePayload } from './payload'
18+
import { ResponseError } from './responseError'
1819

1920
export function errorCodeToString(returnCode: LedgerError): string {
2021
const returnCodeStr = returnCode.toString(16).toUpperCase()
@@ -27,13 +28,6 @@ export function errorCodeToString(returnCode: LedgerError): string {
2728
return errDescription
2829
}
2930

30-
function responseBaseFrom(retCode: number): ResponseReturnCode {
31-
return {
32-
returnCode: retCode,
33-
errorMessage: errorCodeToString(retCode),
34-
}
35-
}
36-
3731
function isDict(v: any): boolean {
3832
return typeof v === 'object' && v !== null && !(v instanceof Array) && !(v instanceof Date)
3933
}
@@ -51,7 +45,7 @@ function isDict(v: any): boolean {
5145
export function processResponse(responseRaw: Buffer): ResponsePayload {
5246
// Ensure the buffer is large enough to contain a return code
5347
if (responseRaw.length < 2) {
54-
throw responseBaseFrom(LedgerError.EmptyBuffer) as ResponseError
48+
throw ResponseError.fromReturnCode(LedgerError.EmptyBuffer)
5549
}
5650

5751
// Determine the return code from the last two bytes of the response
@@ -63,7 +57,7 @@ export function processResponse(responseRaw: Buffer): ResponsePayload {
6357

6458
// Directly return the payload if there are no errors
6559
if (returnCode === LedgerError.NoErrors) {
66-
return payload
60+
return new ResponsePayload(payload)
6761
}
6862

6963
// Append additional error message from payload if available
@@ -84,10 +78,10 @@ export function processResponse(responseRaw: Buffer): ResponsePayload {
8478
* @param response - The raw response object that may contain error details.
8579
* @returns A standardized error response object.
8680
*/
87-
export function processErrorResponse(response: any): ResponseReturnCode {
81+
export function processErrorResponse(response: any): ResponseError {
8882
if (isDict(response)) {
8983
if (Object.prototype.hasOwnProperty.call(response, 'statusCode')) {
90-
return responseBaseFrom(response.statusCode)
84+
return ResponseError.fromReturnCode(response.statusCode)
9185
}
9286

9387
if (Object.prototype.hasOwnProperty.call(response, 'returnCode') && Object.prototype.hasOwnProperty.call(response, 'errorMessage')) {
@@ -96,5 +90,5 @@ export function processErrorResponse(response: any): ResponseReturnCode {
9690
}
9791

9892
// If response is not a dictionary or does not contain the expected properties, handle as unknown error
99-
return responseBaseFrom(LedgerError.UnknownTransportError)
93+
return ResponseError.fromReturnCode(LedgerError.UnknownTransportError)
10094
}

src/payload.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Buffer } from 'buffer'
2+
3+
import { LedgerError } from './consts'
4+
import { ResponsePayload } from './payload'
5+
import { ResponseError } from './responseError'
6+
7+
describe('ResponsePayload', () => {
8+
let payload: Buffer
9+
let responsePayload: ResponsePayload
10+
11+
beforeEach(() => {
12+
payload = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05])
13+
responsePayload = new ResponsePayload(payload)
14+
})
15+
16+
test('getCompleteBuffer should return a complete buffer', () => {
17+
expect(responsePayload.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]))
18+
})
19+
20+
test('getAvailableBuffer should return the available buffer', () => {
21+
responsePayload.readBytes(3)
22+
expect(responsePayload.getAvailableBuffer()).toEqual(Buffer.from([0x04, 0x05]))
23+
})
24+
25+
test('readBytes should return the correct bytes and increase offset', () => {
26+
const readBuffer = responsePayload.readBytes(2)
27+
expect(readBuffer).toEqual(Buffer.from([0x01, 0x02]))
28+
expect(responsePayload.readBytes(1)).toEqual(Buffer.from([0x03]))
29+
})
30+
31+
test('skipBytes should increase the offset correctly', () => {
32+
responsePayload.skipBytes(2)
33+
expect(responsePayload.readBytes(1)).toEqual(Buffer.from([0x03]))
34+
})
35+
36+
test('resetOffset should reset the offset to zero', () => {
37+
responsePayload.readBytes(3)
38+
responsePayload.resetOffset()
39+
expect(responsePayload.readBytes(2)).toEqual(Buffer.from([0x01, 0x02]))
40+
})
41+
42+
test('readBytes should throw an error when reading beyond the buffer length', () => {
43+
expect(() => responsePayload.readBytes(10)).toThrow(new ResponseError(LedgerError.UnknownError, 'Attempt to read beyond buffer length'))
44+
})
45+
46+
test('skipBytes should throw an error when skipping beyond the buffer length', () => {
47+
expect(() => responsePayload.skipBytes(10)).toThrow(new ResponseError(LedgerError.UnknownError, 'Attempt to skip beyond buffer length'))
48+
})
49+
})

0 commit comments

Comments
 (0)