Skip to content

Commit 627d35b

Browse files
authored
Merge pull request #46 from Zondax/feat/process-chunk
Add generic process chunks functions
2 parents 8271989 + a909ce4 commit 627d35b

File tree

7 files changed

+493
-47
lines changed

7 files changed

+493
-47
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ It handles APDU encapsulation, Zemu and USB (HID) communication.
1010
Linux, OSX and Windows are supported.
1111

1212
## Building
13+
1314
```bash
1415
go build
1516
```
17+
18+
## Debug Log
19+
20+
Set the environment variable `LEDGER_LOG_LEVEL` to `debug` to enable debug logging.
21+
22+
```bash
23+
LEDGER_LOG_LEVEL=debug
24+
```

chunking.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*******************************************************************************
2+
* (c) Zondax AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
********************************************************************************/
16+
17+
package ledger_go
18+
19+
import (
20+
"math"
21+
)
22+
23+
const (
24+
// DefaultChunkSize is the standard chunk size used across all Ledger apps
25+
// This replaces userMessageChunkSize from individual apps
26+
DefaultChunkSize = 48
27+
28+
// Chunk payload descriptors
29+
ChunkInit = 0
30+
ChunkAdd = 1
31+
ChunkLast = 2
32+
)
33+
34+
// PrepareChunks splits the transaction data into chunks for sending to the Ledger device
35+
// This matches the exact implementation from ledger-filecoin-go and ledger-avalanche-go
36+
func PrepareChunks(bip44PathBytes []byte, transaction []byte) [][]byte {
37+
var packetIndex = 0
38+
// first chunk + number of chunk needed for transaction
39+
var packetCount = 1 + int(math.Ceil(float64(len(transaction))/float64(DefaultChunkSize)))
40+
41+
chunks := make([][]byte, packetCount)
42+
43+
// First chunk is path
44+
chunks[0] = bip44PathBytes
45+
packetIndex++
46+
47+
for packetIndex < packetCount {
48+
var start = (packetIndex - 1) * DefaultChunkSize
49+
var end = packetIndex * DefaultChunkSize
50+
51+
if end >= len(transaction) {
52+
chunks[packetIndex] = transaction[start:]
53+
} else {
54+
chunks[packetIndex] = transaction[start:end]
55+
}
56+
packetIndex++
57+
}
58+
59+
return chunks
60+
}
61+
62+
// ErrorHandler is a function type for custom error handling in ProcessChunks
63+
type ErrorHandler func(error, []byte, byte) error
64+
65+
// ProcessChunks sends chunks to the Ledger device and collects the response
66+
// This supports both Avalanche and Filecoin error handling patterns via the optional errorHandler
67+
func ProcessChunks(device LedgerDevice, chunks [][]byte, cla, instruction, p2 byte, errorHandler ErrorHandler) ([]byte, error) {
68+
var finalResponse []byte
69+
70+
for chunkIndex, chunk := range chunks {
71+
payloadLen := byte(len(chunk))
72+
payloadDesc := ChunkAdd
73+
74+
if chunkIndex == 0 {
75+
payloadDesc = ChunkInit
76+
} else if chunkIndex == len(chunks)-1 {
77+
payloadDesc = ChunkLast
78+
}
79+
80+
header := []byte{cla, instruction, byte(payloadDesc), p2, payloadLen}
81+
message := append(header, chunk...)
82+
83+
response, err := device.Exchange(message)
84+
if err != nil {
85+
// Use custom error handler if provided
86+
if errorHandler != nil {
87+
return nil, errorHandler(err, response, instruction)
88+
}
89+
return nil, err
90+
}
91+
92+
finalResponse = response
93+
}
94+
95+
return finalResponse, nil
96+
}
97+
98+
// ProcessChunksSimple sends chunks to the Ledger device with basic error handling
99+
// This is a convenience function for apps that don't need custom error handling
100+
func ProcessChunksSimple(device LedgerDevice, chunks [][]byte, cla, instruction, p2 byte) ([]byte, error) {
101+
return ProcessChunks(device, chunks, cla, instruction, p2, nil)
102+
}
103+
104+
// BuildChunkedAPDU builds an APDU command for chunked data transmission
105+
// cla is the APDU class byte
106+
// ins is the APDU instruction byte
107+
// p1 is the APDU P1 parameter (typically the chunk descriptor)
108+
// p2 is the APDU P2 parameter
109+
// data is the chunk data to send
110+
func BuildChunkedAPDU(cla, ins, p1, p2 byte, data []byte) []byte {
111+
// APDU format: [CLA INS P1 P2 LC DATA]
112+
dataLen := len(data)
113+
command := make([]byte, 5+dataLen)
114+
115+
command[0] = cla
116+
command[1] = ins
117+
command[2] = p1
118+
command[3] = p2
119+
command[4] = byte(dataLen)
120+
121+
if dataLen > 0 {
122+
copy(command[5:], data)
123+
}
124+
125+
return command
126+
}

chunking_test.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*******************************************************************************
2+
* (c) Zondax AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
********************************************************************************/
16+
17+
package ledger_go
18+
19+
import (
20+
"bytes"
21+
"errors"
22+
"fmt"
23+
"testing"
24+
)
25+
26+
func TestPrepareChunks(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
bip44PathBytes []byte
30+
transaction []byte
31+
expected int
32+
}{
33+
{
34+
name: "Large transaction",
35+
bip44PathBytes: []byte{0x01, 0x02, 0x03},
36+
transaction: make([]byte, 500),
37+
expected: 12, // 1 path chunk + 11 data chunks (500/48 = 10.4, so 11 chunks)
38+
},
39+
{
40+
name: "Small transaction",
41+
bip44PathBytes: []byte{0x01, 0x02, 0x03},
42+
transaction: make([]byte, 100),
43+
expected: 4, // 1 path chunk + 3 data chunks (100/48 = 2.08, so 3 chunks)
44+
},
45+
{
46+
name: "Exact chunk size transaction",
47+
bip44PathBytes: []byte{0x01, 0x02, 0x03},
48+
transaction: make([]byte, 48),
49+
expected: 2, // 1 path chunk + 1 data chunk
50+
},
51+
{
52+
name: "One byte over chunk size",
53+
bip44PathBytes: []byte{0x01, 0x02, 0x03},
54+
transaction: make([]byte, 49),
55+
expected: 3, // 1 path chunk + 2 data chunks
56+
},
57+
{
58+
name: "Empty transaction",
59+
bip44PathBytes: []byte{0x01, 0x02, 0x03},
60+
transaction: []byte{},
61+
expected: 1, // 1 path chunk only
62+
},
63+
}
64+
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
chunks := PrepareChunks(tt.bip44PathBytes, tt.transaction)
68+
69+
if len(chunks) != tt.expected {
70+
t.Errorf("Expected %d chunks, got %d", tt.expected, len(chunks))
71+
}
72+
73+
// Verify first chunk is the BIP44 path
74+
if !bytes.Equal(chunks[0], tt.bip44PathBytes) {
75+
t.Error("First chunk doesn't match BIP44 path")
76+
}
77+
78+
// Verify transaction data is properly chunked
79+
if len(tt.transaction) > 0 {
80+
reconstructed := make([]byte, 0, len(tt.transaction))
81+
for i := 1; i < len(chunks); i++ {
82+
reconstructed = append(reconstructed, chunks[i]...)
83+
}
84+
85+
if !bytes.Equal(reconstructed, tt.transaction) {
86+
t.Error("Transaction data not properly chunked")
87+
}
88+
}
89+
})
90+
}
91+
}
92+
93+
func TestBuildChunkedAPDU(t *testing.T) {
94+
tests := []struct {
95+
name string
96+
cla byte
97+
ins byte
98+
p1 byte
99+
p2 byte
100+
data []byte
101+
expected []byte
102+
}{
103+
{
104+
name: "Basic APDU",
105+
cla: 0x80,
106+
ins: 0x02,
107+
p1: ChunkInit,
108+
p2: 0x00,
109+
data: []byte{0x01, 0x02, 0x03},
110+
expected: []byte{0x80, 0x02, 0x00, 0x00, 0x03, 0x01, 0x02, 0x03},
111+
},
112+
{
113+
name: "Empty data",
114+
cla: 0x80,
115+
ins: 0x02,
116+
p1: ChunkLast,
117+
p2: 0x00,
118+
data: []byte{},
119+
expected: []byte{0x80, 0x02, 0x02, 0x00, 0x00},
120+
},
121+
}
122+
123+
for _, tt := range tests {
124+
t.Run(tt.name, func(t *testing.T) {
125+
result := BuildChunkedAPDU(tt.cla, tt.ins, tt.p1, tt.p2, tt.data)
126+
127+
if !bytes.Equal(result, tt.expected) {
128+
t.Errorf("Expected %X, got %X", tt.expected, result)
129+
}
130+
})
131+
}
132+
}
133+
134+
// MockLedgerDevice for testing ProcessChunks
135+
type MockLedgerDevice struct {
136+
responses [][]byte
137+
callCount int
138+
commands [][]byte
139+
}
140+
141+
func (m *MockLedgerDevice) Exchange(command []byte) ([]byte, error) {
142+
m.commands = append(m.commands, command)
143+
if m.callCount < len(m.responses) {
144+
response := m.responses[m.callCount]
145+
m.callCount++
146+
return response, nil
147+
}
148+
return []byte{0x90, 0x00}, nil
149+
}
150+
151+
func (m *MockLedgerDevice) Close() error {
152+
return nil
153+
}
154+
155+
func TestProcessChunks(t *testing.T) {
156+
mockDevice := &MockLedgerDevice{
157+
responses: [][]byte{
158+
{0x01, 0x02},
159+
{0x03, 0x04},
160+
{0x05, 0x06},
161+
},
162+
}
163+
164+
chunks := [][]byte{
165+
{0xAA, 0xBB},
166+
{0xCC, 0xDD},
167+
{0xEE, 0xFF},
168+
}
169+
170+
response, err := ProcessChunksSimple(mockDevice, chunks, 0x80, 0x02, 0x00)
171+
if err != nil {
172+
t.Fatalf("ProcessChunks failed: %v", err)
173+
}
174+
175+
// Check that we got the last response
176+
if !bytes.Equal(response, []byte{0x05, 0x06}) {
177+
t.Errorf("Expected last response, got %X", response)
178+
}
179+
180+
// Check number of calls
181+
if mockDevice.callCount != 3 {
182+
t.Errorf("Expected 3 Exchange calls, got %d", mockDevice.callCount)
183+
}
184+
185+
// Check P1 values for chunk descriptors
186+
expectedP1 := []byte{ChunkInit, ChunkAdd, ChunkLast}
187+
for i, cmd := range mockDevice.commands {
188+
if cmd[2] != expectedP1[i] {
189+
t.Errorf("Chunk %d: expected P1=%X, got %X", i, expectedP1[i], cmd[2])
190+
}
191+
}
192+
}
193+
194+
// MockErrorDevice for testing error handling
195+
type MockErrorDevice struct {
196+
errorToReturn error
197+
responseBytes []byte
198+
}
199+
200+
func (m *MockErrorDevice) Exchange(command []byte) ([]byte, error) {
201+
return m.responseBytes, m.errorToReturn
202+
}
203+
204+
func (m *MockErrorDevice) Close() error {
205+
return nil
206+
}
207+
208+
func TestProcessChunksWithErrorHandler(t *testing.T) {
209+
mockDevice := &MockErrorDevice{
210+
errorToReturn: errors.New("[APDU_CODE_BAD_KEY_HANDLE] The parameters in the data field are incorrect"),
211+
responseBytes: []byte{0xAB, 0xCD},
212+
}
213+
214+
chunks := [][]byte{
215+
{0xAA, 0xBB},
216+
}
217+
218+
// Test with custom error handler (similar to Avalanche)
219+
_, err := ProcessChunks(mockDevice, chunks, 0x80, 0x02, 0x00, func(err error, response []byte, instruction byte) error {
220+
if instruction == 0x02 && err.Error() == "[APDU_CODE_BAD_KEY_HANDLE] The parameters in the data field are incorrect" {
221+
return fmt.Errorf("%w extra_info=(%s)", err, string(response))
222+
}
223+
return err
224+
})
225+
226+
if err == nil {
227+
t.Fatal("Expected error but got none")
228+
}
229+
230+
if !bytes.Contains([]byte(err.Error()), []byte("extra_info=")) {
231+
t.Errorf("Expected custom error handling with extra_info, got: %v", err)
232+
}
233+
}

0 commit comments

Comments
 (0)