Skip to content

Commit 3d3ebc7

Browse files
authored
refactor path serialization to make it more reusable (#21)
1 parent 8ee4450 commit 3d3ebc7

File tree

6 files changed

+124
-80
lines changed

6 files changed

+124
-80
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
same "printed page" as the copyright notice for easier
187187
identification within third-party archives.
188188

189-
Copyright 2022 Zondax AG
189+
Copyright 2022-2024 Zondax AG
190190

191191
Licensed under the Apache License, Version 2.0 (the "License");
192192
you may not use this file except in compliance with the License.

src/app.test.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,9 @@ describe('BaseApp', () => {
2525
ins: { GET_VERSION: 0x00 as 0 },
2626
p1Values: { ONLY_RETRIEVE: 0x00 as 0, SHOW_ADDRESS_IN_DEVICE: 0x01 as 1 },
2727
chunkSize: 255,
28-
acceptedPathLengths: [3, 5],
28+
requiredPathLengths: [3, 5],
2929
}
3030

31-
describe('serializePath', () => {
32-
it('should throw an error if path does not start with "m/"', () => {
33-
const transport = new MockTransport(Buffer.alloc(0))
34-
const app = new BaseApp(transport, params)
35-
expect(() => app.serializePath("44'/0'/0'")).toThrow('Path should start with "m/"')
36-
})
37-
38-
it('should throw an error if path length is not accepted', () => {
39-
const transport = new MockTransport(Buffer.alloc(0))
40-
const app = new BaseApp(transport, params)
41-
expect(() => app.serializePath("m/44'/0'")).toThrow('Invalid path.')
42-
})
43-
44-
it('should correctly serialize a valid path', () => {
45-
const transport = new MockTransport(Buffer.alloc(0))
46-
const app = new BaseApp(transport, params)
47-
const path = "m/44'/0'/0'"
48-
const buffer = app.serializePath(path)
49-
expect(buffer.length).toBe(12)
50-
expect(buffer.readUInt32LE(0)).toBe(0x8000002c)
51-
expect(buffer.readUInt32LE(4)).toBe(0x80000000)
52-
expect(buffer.readUInt32LE(8)).toBe(0x80000000)
53-
})
54-
})
55-
5631
describe('prepareChunks', () => {
5732
it('should prepare chunks correctly', () => {
5833
const transport = new MockTransport(Buffer.alloc(0))

src/app.ts

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
*****************************************************************************/
1616
import type Transport from '@ledgerhq/hw-transport'
1717

18+
import { serializePath } from './bip32'
1819
import { processErrorResponse, processResponse } from './common'
19-
import { HARDENED, LEDGER_DASHBOARD_CLA, LedgerError, PAYLOAD_TYPE } from './consts'
20+
import { LEDGER_DASHBOARD_CLA, LedgerError, PAYLOAD_TYPE } from './consts'
2021
import { ResponsePayload } from './payload'
2122
import { ResponseError } from './responseError'
2223
import {
@@ -33,7 +34,7 @@ export default class BaseApp {
3334
readonly CLA: number
3435
readonly INS: INSGeneric
3536
readonly P1_VALUES: P1_VALUESGeneric
36-
readonly ACCEPTED_PATH_LENGTHS?: number[]
37+
readonly REQUIRED_PATH_LENGTHS?: number[]
3738
readonly CHUNK_SIZE: number
3839

3940
constructor(transport: Transport, params: ConstructorParams) {
@@ -42,56 +43,7 @@ export default class BaseApp {
4243
this.INS = params.ins
4344
this.P1_VALUES = params.p1Values
4445
this.CHUNK_SIZE = params.chunkSize
45-
this.ACCEPTED_PATH_LENGTHS = params.acceptedPathLengths
46-
}
47-
48-
/**
49-
* Serializes a derivation path into a buffer.
50-
* @param path - The derivation path in string format.
51-
* @returns A buffer representing the serialized path.
52-
* @throws {Error} If the path format is incorrect or invalid.
53-
*/
54-
serializePath(path: string): Buffer {
55-
if (typeof path !== 'string') {
56-
throw new Error("Path should be a string (e.g \"m/44'/461'/5'/0/3\")")
57-
}
58-
59-
if (!path.startsWith('m/')) {
60-
throw new Error('Path should start with "m/" (e.g "m/44\'/5757\'/5\'/0/3")')
61-
}
62-
63-
const pathArray = path.split('/')
64-
pathArray.shift() // remove "m"
65-
66-
if (this.ACCEPTED_PATH_LENGTHS && !this.ACCEPTED_PATH_LENGTHS.includes(pathArray.length)) {
67-
throw new Error("Invalid path. (e.g \"m/44'/5757'/5'/0/3\")")
68-
}
69-
70-
const buf = Buffer.alloc(4 * pathArray.length)
71-
72-
pathArray.forEach((child: string, i: number) => {
73-
let value = 0
74-
75-
if (child.endsWith("'")) {
76-
value += HARDENED
77-
child = child.slice(0, -1)
78-
}
79-
80-
const numChild = Number(child)
81-
82-
if (Number.isNaN(numChild)) {
83-
throw new Error(`Invalid path : ${child} is not a number. (e.g "m/44'/461'/5'/0/3")`)
84-
}
85-
86-
if (numChild >= HARDENED) {
87-
throw new Error('Incorrect child value (bigger or equal to 0x80000000)')
88-
}
89-
90-
value += numChild
91-
buf.writeUInt32LE(value, 4 * i)
92-
})
93-
94-
return buf
46+
this.REQUIRED_PATH_LENGTHS = params.acceptedPathLengths
9547
}
9648

9749
/**
@@ -102,7 +54,7 @@ export default class BaseApp {
10254
*/
10355
prepareChunks(path: string, message: Buffer): Buffer[] {
10456
const chunks = []
105-
const serializedPathBuffer = this.serializePath(path)
57+
const serializedPathBuffer = serializePath(path, this.REQUIRED_PATH_LENGTHS)
10658

10759
// First chunk (only path)
10860
chunks.push(serializedPathBuffer)

src/bip32.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { serializePath } from './bip32'
2+
3+
test('serializePath - valid path', async () => {
4+
const path = "m/44'/461'/0/0/5"
5+
const buf = Buffer.alloc(20)
6+
buf.writeUInt32LE(0x80000000 + 44, 0)
7+
buf.writeUInt32LE(0x80000000 + 461, 4)
8+
buf.writeUInt32LE(0, 8)
9+
buf.writeUInt32LE(0, 12)
10+
buf.writeUInt32LE(5, 16)
11+
12+
const bufPath = serializePath(path)
13+
14+
expect(bufPath).toEqual(buf)
15+
})
16+
17+
test('serializePath - path should be a string', async () => {
18+
const path = [44, 461, 0, 2, 3]
19+
20+
expect(() => {
21+
serializePath(path as unknown as string)
22+
}).toThrow(/Path should be a string/)
23+
})
24+
25+
test("serializePath - path should start with 'm'", async () => {
26+
const path = "/44'/461'/0/0/5"
27+
28+
expect(() => {
29+
serializePath(path)
30+
}).toThrow(/Path should start with "m\/"/)
31+
})
32+
33+
test('serializePath - path length needs to be 5', async () => {
34+
const path = "m/44'/461'/0/0"
35+
36+
expect(() => {
37+
serializePath(path, [5])
38+
}).toThrow(/Invalid path/)
39+
})
40+
41+
test('serializePath - invalid number in path', async () => {
42+
const path = "m/44'/461'/0/0/l"
43+
44+
expect(() => {
45+
serializePath(path)
46+
}).toThrow(/Invalid path : l is not a number/)
47+
})
48+
49+
test('serializePath - child value should not be bigger than 0x80000000', async () => {
50+
const path = "m/44'/461'/0/0/2147483648"
51+
52+
expect(() => {
53+
serializePath(path)
54+
}).toThrow('Incorrect child value (bigger or equal to 0x80000000)')
55+
})
56+
57+
test('serializePath - child value should not be bigger than 0x80000000', async () => {
58+
const path = "m/44'/461'/0/0/2147483649"
59+
60+
expect(() => {
61+
serializePath(path)
62+
}).toThrow('Incorrect child value (bigger or equal to 0x80000000)')
63+
})

src/bip32.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { HARDENED, LedgerError } from './consts'
2+
import { ResponseError } from './responseError'
3+
4+
/**
5+
* Serializes a derivation path into a buffer.
6+
* @param path - The derivation path in string format.
7+
* @returns A buffer representing the serialized path.
8+
* @throws {Error} If the path format is incorrect or invalid.
9+
*/
10+
export function serializePath(path: string, requiredPathLengths?: number[]): Buffer {
11+
if (typeof path !== 'string') {
12+
// NOTE: this is probably unnecessary
13+
throw new ResponseError(LedgerError.GenericError, "Path should be a string (e.g \"m/44'/461'/5'/0/3\")")
14+
}
15+
16+
if (!path.startsWith('m/')) {
17+
throw new ResponseError(LedgerError.GenericError, 'Path should start with "m/" (e.g "m/44\'/461\'/5\'/0/3")')
18+
}
19+
20+
const pathArray = path.split('/')
21+
pathArray.shift() // remove "m"
22+
23+
if (requiredPathLengths && requiredPathLengths.length > 0 && !requiredPathLengths.includes(pathArray.length)) {
24+
throw new ResponseError(LedgerError.GenericError, "Invalid path length. (e.g \"m/44'/5757'/5'/0/3\")")
25+
}
26+
27+
const buf = Buffer.alloc(4 * pathArray.length)
28+
pathArray.forEach((child: string, i: number) => {
29+
let value = 0
30+
31+
if (child.endsWith("'")) {
32+
value += HARDENED
33+
child = child.slice(0, -1)
34+
}
35+
36+
const numChild = Number(child)
37+
38+
if (Number.isNaN(numChild)) {
39+
throw new ResponseError(LedgerError.GenericError, `Invalid path : ${child} is not a number. (e.g "m/44'/461'/5'/0/3")`)
40+
}
41+
42+
if (numChild >= HARDENED) {
43+
throw new ResponseError(LedgerError.GenericError, 'Incorrect child value (bigger or equal to 0x80000000)')
44+
}
45+
46+
value += numChild
47+
buf.writeUInt32LE(value, 4 * i)
48+
})
49+
50+
return buf
51+
}

src/consts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export const enum LedgerError {
8787
FileNotFound = 0x9404,
8888
UserRefusedOnDevice = 0x5501,
8989
NotEnoughSpace = 0x5102,
90+
91+
GenericError = 0xffffffff,
9092
}
9193

9294
export const ERROR_DESCRIPTION_OVERRIDE: Readonly<Record<LedgerError, string>> = {
@@ -144,4 +146,5 @@ export const ERROR_DESCRIPTION_OVERRIDE: Readonly<Record<LedgerError, string>> =
144146
[LedgerError.FileNotFound]: 'File Not Found',
145147
[LedgerError.UserRefusedOnDevice]: 'User Refused on Device',
146148
[LedgerError.NotEnoughSpace]: 'Not Enough Space',
149+
[LedgerError.GenericError]: 'Generic Error',
147150
}

0 commit comments

Comments
 (0)