├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd └── ledger │ └── main.go ├── comm.go ├── comm_test.go ├── device.go ├── glide.lock ├── glide.yaml ├── helper_test.go ├── wrapper.go └── wrapper_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Ledger Go bindings 2 | License: Apache2.0 3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "{}" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright 2016 All in Bits, Inc 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install test vendor 2 | 3 | install: 4 | go install ./cmd/ledger 5 | 6 | test: 7 | @go test . 8 | 9 | vendor: 10 | @go get github.com/Masterminds/glide 11 | @glide install 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go bindings for Ledger Nano 2 | 3 | These are simple go bindings to communicate with custom Ledger Nano apps. 4 | This wraps the USB HID layer and handles the ledger specific communication. 5 | 6 | ## CLI Usage 7 | 8 | Send bytes to ledger (app 0x80, op 0x02, payload 0xf00d) 9 | 10 | ``` 11 | make vendor 12 | make install 13 | ledger 8002F00D 14 | ``` 15 | 16 | 17 | ## API Usage 18 | 19 | ``` 20 | import "github.com/ethanfrey/ledger" 21 | 22 | func PingLedger(msg []byte) ([]byte, error) { 23 | device, err := ledger.FindLedger() 24 | if err != nil { 25 | return nil, err 26 | } 27 | return device.Exchange(msg, 0) 28 | } 29 | ``` 30 | 31 | ## TODO 32 | 33 | * Actually use the timeout parameter 34 | * Build higher level constructs for other apps 35 | -------------------------------------------------------------------------------- /cmd/ledger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/ethanfrey/ledger" 9 | ) 10 | 11 | func main() { 12 | ledger, err := ledger.FindLedger() 13 | if err != nil { 14 | fmt.Printf("Error: %+v\n", err) 15 | return 16 | } 17 | 18 | data, err := hex.DecodeString(os.Args[1]) 19 | if err != nil { 20 | fmt.Printf("Error: %+v\n", err) 21 | return 22 | } 23 | fmt.Printf("Sending %X\n\n", data) 24 | 25 | resp, err := ledger.Exchange(data, 100) 26 | if err != nil { 27 | fmt.Printf("Error: %+v\n", err) 28 | return 29 | } 30 | fmt.Printf("Response: %X\n", resp) 31 | } 32 | -------------------------------------------------------------------------------- /comm.go: -------------------------------------------------------------------------------- 1 | package ledger 2 | 3 | import "fmt" 4 | 5 | func (l *Ledger) Exchange(command []byte, timeout int) ([]byte, error) { 6 | adpu := WrapCommandAPDU(Channel, command, PacketSize, false) 7 | 8 | // write all the packets 9 | err := l.device.Write(adpu[:PacketSize]) 10 | if err != nil { 11 | return nil, err 12 | } 13 | for len(adpu) > PacketSize { 14 | adpu = adpu[PacketSize:] 15 | err = l.device.Write(adpu[:PacketSize]) 16 | if err != nil { 17 | return nil, err 18 | } 19 | } 20 | 21 | input := l.device.ReadCh() 22 | response, err := UnwrapResponseAPDU(Channel, input, PacketSize, false) 23 | 24 | swOffset := len(response) - 2 25 | sw := codec.Uint16(response[swOffset:]) 26 | if sw != 0x9000 { 27 | return nil, fmt.Errorf("Invalid status %04x", sw) 28 | } 29 | return response[:swOffset], nil 30 | } 31 | -------------------------------------------------------------------------------- /comm_test.go: -------------------------------------------------------------------------------- 1 | package ledger 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestExchange(t *testing.T) { 11 | assert, require := assert.New(t), require.New(t) 12 | 13 | cases := []struct { 14 | input string 15 | }{ 16 | {"food"}, 17 | {"q4w35ertdyfugihojpkdryftughiuj"}, 18 | { 19 | `this is a very long message... oh so long 20 | that it splits over many many lines. 21 | q43w5e65rtiyuoporaestdyfugihoijrdytfuygih 22 | weurityuoisyrdutfiuyoio5w4e6r7t8y9udrytfuygiuhij`, 23 | }, 24 | } 25 | 26 | for i, tc := range cases { 27 | data := []byte(tc.input) 28 | 29 | // no 0x9000 trailer... 30 | echo := NewLedger(NewEcho(64)) 31 | resp, err := echo.Exchange(data, 100) 32 | require.NotNil(err) 33 | 34 | // note: we need to append 9000 for success 35 | echo = NewLedger(NewEcho(64)) 36 | send := append(data, 0x90, 0x0) 37 | resp, err = echo.Exchange(send, 100) 38 | require.Nil(err, "%d: %+v", i, err) 39 | assert.Equal(data, resp, "%d", i) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /device.go: -------------------------------------------------------------------------------- 1 | package ledger 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ethanfrey/hid" 7 | ) 8 | 9 | const ( 10 | VendorLedger = 0x2c97 11 | ProductNano = 1 12 | Channel = 0x0101 13 | PacketSize = 64 14 | ) 15 | 16 | type Ledger struct { 17 | device Device 18 | } 19 | 20 | func NewLedger(dev Device) *Ledger { 21 | return &Ledger{ 22 | device: dev, 23 | } 24 | } 25 | 26 | func FindLedger() (*Ledger, error) { 27 | devs, err := hid.Devices() 28 | if err != nil { 29 | return nil, err 30 | } 31 | for _, d := range devs { 32 | // TODO: ProductId filter 33 | if d.VendorID == VendorLedger { 34 | ledger, err := d.Open() 35 | if err != nil { 36 | return nil, err 37 | } 38 | return NewLedger(ledger), nil 39 | } 40 | } 41 | return nil, errors.New("no ledger connected") 42 | } 43 | 44 | // A Device provides access to a HID device. 45 | type Device interface { 46 | // Close closes the device and associated resources. 47 | Close() 48 | 49 | // Write writes an output report to device. The first byte must be the 50 | // report number to write, zero if the device does not use numbered reports. 51 | Write([]byte) error 52 | 53 | // ReadCh returns a channel that will be sent input reports from the device. 54 | // If the device uses numbered reports, the first byte will be the report 55 | // number. 56 | ReadCh() <-chan []byte 57 | 58 | // ReadError returns the read error, if any after the channel returned from 59 | // ReadCh has been closed. 60 | ReadError() error 61 | } 62 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 91421d509e2fccc2fe33b0ecc56d01badc182070b2eddd4ddf152be51c53c2f2 2 | updated: 2017-11-22T11:48:34.18875617+01:00 3 | imports: 4 | - name: github.com/ethanfrey/hid 5 | version: dce75af84cf849c60f39bf1b8192fb5cd597b5d9 6 | testImports: 7 | - name: github.com/davecgh/go-spew 8 | version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 9 | subpackages: 10 | - spew 11 | - name: github.com/pmezard/go-difflib 12 | version: d8ed2627bdf02c080bf22230dbb337003b7aba2d 13 | subpackages: 14 | - difflib 15 | - name: github.com/stretchr/testify 16 | version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0 17 | subpackages: 18 | - assert 19 | - require 20 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/ethanfrey/ledger 2 | import: 3 | - package: github.com/ethanfrey/hid 4 | testImport: 5 | - package: github.com/stretchr/testify 6 | subpackages: 7 | - assert 8 | - require 9 | 10 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package ledger 2 | 3 | type EchoDevice struct { 4 | chunk int 5 | data [][]byte 6 | } 7 | 8 | func NewEcho(chunk int) *EchoDevice { 9 | return &EchoDevice{ 10 | chunk: chunk, 11 | } 12 | } 13 | 14 | func (e *EchoDevice) Write(input []byte) error { 15 | for len(input) > e.chunk { 16 | e.data = append(e.data, input[:e.chunk]) 17 | input = input[e.chunk:] 18 | } 19 | pad := len(input) - e.chunk 20 | if pad > 0 { 21 | input = append(input, make([]byte, pad)...) 22 | } 23 | e.data = append(e.data, input) 24 | return nil 25 | } 26 | 27 | func (e *EchoDevice) Close() {} 28 | func (e *EchoDevice) ReadError() error { return nil } 29 | 30 | func (e *EchoDevice) ReadCh() <-chan []byte { 31 | output := make(chan []byte, 3) 32 | go func() { 33 | buf := e.data 34 | for i := 0; i < len(buf); i++ { 35 | output <- buf[i] 36 | } 37 | }() 38 | return output 39 | } 40 | -------------------------------------------------------------------------------- /wrapper.go: -------------------------------------------------------------------------------- 1 | package ledger 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | ) 7 | 8 | var codec = binary.BigEndian 9 | 10 | // WrapCommandAPDU turns the command into a sequence of 64 byte packets 11 | func WrapCommandAPDU(channel uint16, command []byte, packetSize int, ble bool) []byte { 12 | if packetSize < 3 { 13 | panic("packet size must be at least 3") 14 | } 15 | 16 | var sequenceIdx uint16 17 | var offset, extraHeaderSize, blockSize int 18 | var result = make([]byte, 64) 19 | var buf = result 20 | 21 | if !ble { 22 | codec.PutUint16(buf, channel) 23 | extraHeaderSize = 2 24 | buf = buf[2:] 25 | } 26 | 27 | buf[0] = 0x05 28 | codec.PutUint16(buf[1:], sequenceIdx) 29 | codec.PutUint16(buf[3:], uint16(len(command))) 30 | sequenceIdx++ 31 | buf = buf[5:] 32 | 33 | blockSize = packetSize - 5 - extraHeaderSize 34 | copy(buf, command) 35 | offset += blockSize 36 | 37 | for offset < len(command) { 38 | // TODO: optimize this 39 | end := len(result) 40 | result = append(result, make([]byte, 64)...) 41 | buf = result[end:] 42 | if !ble { 43 | codec.PutUint16(buf, channel) 44 | buf = buf[2:] 45 | } 46 | buf[0] = 0x05 47 | codec.PutUint16(buf[1:], sequenceIdx) 48 | sequenceIdx++ 49 | buf = buf[3:] 50 | 51 | blockSize = packetSize - 3 - extraHeaderSize 52 | copy(buf, command[offset:]) 53 | offset += blockSize 54 | } 55 | 56 | return result 57 | } 58 | 59 | var ( 60 | errTooShort = errors.New("too short") 61 | errInvalidChannel = errors.New("invalid channel") 62 | errInvalidSequence = errors.New("invalid sequence") 63 | errInvalidTag = errors.New("invalid tag") 64 | ) 65 | 66 | func validatePrefix(buf []byte, channel, sequenceIdx uint16, ble bool) ([]byte, error) { 67 | if !ble { 68 | if codec.Uint16(buf) != channel { 69 | return nil, errInvalidChannel 70 | } 71 | buf = buf[2:] 72 | } 73 | 74 | if buf[0] != 0x05 { 75 | return nil, errInvalidTag 76 | } 77 | if codec.Uint16(buf[1:]) != sequenceIdx { 78 | return nil, errInvalidSequence 79 | } 80 | return buf[3:], nil 81 | } 82 | 83 | // UnwrapResponseAPDU parses a response of 64 byte packets into the real data 84 | func UnwrapResponseAPDU(channel uint16, dev <-chan []byte, packetSize int, ble bool) ([]byte, error) { 85 | var err error 86 | var sequenceIdx uint16 87 | var extraHeaderSize int 88 | if !ble { 89 | extraHeaderSize = 2 90 | } 91 | buf := <-dev 92 | if len(buf) < 5+extraHeaderSize+5 { 93 | return nil, errTooShort 94 | } 95 | 96 | buf, err = validatePrefix(buf, channel, sequenceIdx, ble) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | responseLength := int(codec.Uint16(buf)) 102 | buf = buf[2:] 103 | result := make([]byte, responseLength) 104 | out := result 105 | 106 | blockSize := packetSize - 5 - extraHeaderSize 107 | if blockSize > len(buf) { 108 | blockSize = len(buf) 109 | } 110 | copy(out, buf[:blockSize]) 111 | 112 | // if there is anything left to read... 113 | for len(out) > blockSize { 114 | out = out[blockSize:] 115 | buf = <-dev 116 | 117 | sequenceIdx++ 118 | buf, err = validatePrefix(buf, channel, sequenceIdx, ble) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | blockSize = packetSize - 3 - extraHeaderSize 124 | if blockSize > len(buf) { 125 | blockSize = len(buf) 126 | } 127 | copy(out, buf[:blockSize]) 128 | } 129 | return result, nil 130 | } 131 | -------------------------------------------------------------------------------- /wrapper_test.go: -------------------------------------------------------------------------------- 1 | package ledger 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestWrapCommand(t *testing.T) { 12 | assert, require := assert.New(t), require.New(t) 13 | 14 | channel := uint16(0x0101) 15 | size := 64 16 | 17 | cases := []struct { 18 | input string 19 | output string 20 | }{ 21 | {"31323334353637383930", 22 | "0101050000000a313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, 23 | {"74686973206973206120612062756e6368206f662072616e646f6d2064617461", 24 | "0101050000002074686973206973206120612062756e6368206f662072616e646f6d206461746100000000000000000000000000000000000000000000000000"}, 25 | {"6d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e646d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e646d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e646d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e646d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e646d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e64", 26 | "010105000000e46d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e646d6f7265207468616e2036342062797465732c010105000120736f2069742077726170732061726f756e646d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e646d6f01010500027265207468616e2036342062797465732c20736f2069742077726170732061726f756e646d6f7265207468616e2036342062797465732c20736f20010105000369742077726170732061726f756e646d6f7265207468616e2036342062797465732c20736f2069742077726170732061726f756e64000000000000"}, 27 | {"deadbeef1234560000deadbeef123456000000dead", 28 | "01010500000015deadbeef1234560000deadbeef123456000000dead000000000000000000000000000000000000000000000000000000000000000000000000"}, 29 | } 30 | 31 | for i, tc := range cases { 32 | hexIn, err := hex.DecodeString(tc.input) 33 | require.Nil(err, "%d: %+v", i, err) 34 | hexOut, err := hex.DecodeString(tc.output) 35 | require.Nil(err, "%d: %+v", i, err) 36 | 37 | msg := WrapCommandAPDU(channel, hexIn, size, false) 38 | assert.Equal(hexOut, msg, "%d", i) 39 | 40 | machine := NewEcho(size) 41 | machine.Write(msg) 42 | resp, err := UnwrapResponseAPDU(channel, machine.ReadCh(), size, false) 43 | assert.Nil(err, "%d: %+v", i, err) 44 | assert.Equal(hexIn, resp, "%d", i) 45 | } 46 | } 47 | --------------------------------------------------------------------------------