├── .github ├── dependabot.yml └── workflows │ ├── checks.golang.yml │ └── main.yml ├── .gitignore ├── .vscode └── settings.json ├── FUNDING.json ├── LICENSE ├── Makefile ├── README.md ├── apduWrapper.go ├── apduWrapper_test.go ├── go.mod ├── go.sum ├── ledger.go ├── ledger_hid.go ├── ledger_mock.go ├── ledger_test.go ├── ledger_zemu.go ├── zemu.pb.go └── zemu.proto /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Enable version updates for npm 9 | - package-ecosystem: 'gomod' 10 | # Look for `package.json` and `lock` files in the `root` directory 11 | directory: '/' 12 | # Check the npm registry for updates every day (weekdays) 13 | schedule: 14 | interval: 'daily' 15 | commit-message: 16 | prefix: 'chore' 17 | prefix-development: 'chore' 18 | include: 'scope' 19 | target-branch: dev 20 | -------------------------------------------------------------------------------- /.github/workflows/checks.golang.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Generated by @zondax/cli 3 | # 4 | name: Checks 5 | 6 | on: 7 | push: 8 | branches: [ main, dev ] 9 | pull_request: 10 | branches: [ main, dev ] 11 | 12 | jobs: 13 | checks: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | submodules: true 20 | - uses: actions/setup-go@v3 21 | with: 22 | go-version: '1.21' 23 | - name: Build 24 | run: | 25 | make build 26 | - name: ModTidy check 27 | run: make check-modtidy 28 | - name: Lint check 29 | run: | 30 | export PATH=$PATH:$(go env GOPATH)/bin 31 | make install_lint 32 | make lint 33 | - name: Run tests 34 | run: | 35 | make test -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the main branch 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | jobs: 16 | 17 | Test: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-go@v3 22 | with: 23 | go-version: '1.21' 24 | - name: Test 25 | run: | 26 | go test -tags ledger_mock 27 | - name: Build 28 | run: | 29 | go build 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files - Why? For now, Allow go.mod 20 | #*.mod 21 | 22 | # Fortran module files 23 | *.smod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | 36 | # OS related files 37 | .DS_Store 38 | 39 | # Others 40 | cmake-build-debug/ 41 | \.idea/workspace\.xml 42 | \.idea/ 43 | vendor/ 44 | 45 | coverage.txt 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.testFlags": [ 3 | "-tags=ledger_mock", 4 | "-v", 5 | "-race", 6 | "-coverprofile=coverage.txt", 7 | "-covermode=atomic" 8 | ], 9 | "go.coverOnSave": true, 10 | "go.coverageOptions": "showUncoveredCodeOnly", 11 | "go.coverageDecorator": { 12 | "type": "gutter", 13 | "coveredHighlightColor": "rgba(64,128,128,0.5)", 14 | "uncoveredHighlightColor": "rgba(128,64,64,0.25)", 15 | "coveredGutterStyle": "blockblue", 16 | "uncoveredGutterStyle": "slashyellow" 17 | } 18 | } -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0x8D8D391c6690C0d0BDe73CB7Ef8B7655e68efe76" 5 | }, 6 | "filecoin": { 7 | "ownedBy": "0x8D8D391c6690C0d0BDe73CB7Ef8B7655e68efe76" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright Zondax AG 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Generated by @zondax/cli 3 | # 4 | -include Makefile.settings.mk 5 | 6 | # Get all directories under cmd 7 | CMDS=$(shell find cmd -type d) 8 | 9 | # Strip cmd/ from directory names and generate output binary names 10 | BINS=$(subst cmd/,output/,$(CMDS)) 11 | 12 | default: build 13 | 14 | build: 15 | @go build 16 | 17 | mod-tidy: 18 | @go mod tidy 19 | 20 | mod-clean: 21 | @go clean -modcache 22 | 23 | mod-update: mod-clean 24 | @go get -u -t all 25 | @go mod tidy 26 | 27 | generate: mod-tidy 28 | @go generate ./internal/... 29 | 30 | version: build 31 | ./output/$(APP_NAME) version 32 | 33 | clean: 34 | go clean 35 | 36 | gitclean: 37 | git clean -xfd 38 | git submodule foreach --recursive git clean -xfd 39 | 40 | install_lint: 41 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin latest 42 | 43 | check-modtidy: 44 | go mod tidy 45 | git diff --exit-code -- go.mod go.sum 46 | 47 | lint: 48 | golangci-lint --version 49 | golangci-lint run 50 | 51 | test: 52 | go test -tags ledger_mock -v -race ./... -coverprofile=coverage.txt -covermode=atomic 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ledger-go 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | [![GithubActions](https://github.com/zondax/ledger-go/actions/workflows/main.yml/badge.svg)](https://github.com/Zondax/ledger-go/blob/main/.github/workflows/main.yml) 5 | 6 | This project provides a library to connect to ledger devices. 7 | 8 | It handles APDU encapsulation, Zemu and USB (HID) communication. 9 | 10 | Linux, OSX and Windows are supported. 11 | 12 | ## Building 13 | ```bash 14 | go build 15 | ``` 16 | -------------------------------------------------------------------------------- /apduWrapper.go: -------------------------------------------------------------------------------- 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 | "encoding/binary" 21 | "fmt" 22 | 23 | "github.com/pkg/errors" 24 | ) 25 | 26 | const ( 27 | MinPacketSize = 3 28 | TagValue = 0x05 29 | ) 30 | 31 | var codec = binary.BigEndian 32 | 33 | const ( 34 | ErrMsgPacketSize = "packet size must be at least 3" 35 | ErrMsgInvalidChannel = "invalid channel" 36 | ErrMsgInvalidTag = "invalid tag" 37 | ErrMsgWrongSequenceIdx = "wrong sequenceIdx" 38 | ) 39 | 40 | var ( 41 | ErrPacketSize = errors.New(ErrMsgPacketSize) 42 | ErrInvalidChannel = errors.New(ErrMsgInvalidChannel) 43 | ErrInvalidTag = errors.New(ErrMsgInvalidTag) 44 | ErrWrongSequenceIdx = errors.New(ErrMsgWrongSequenceIdx) 45 | ) 46 | 47 | // ErrorMessage returns a human-readable error message for a given APDU error code. 48 | func ErrorMessage(errorCode uint16) string { 49 | switch errorCode { 50 | // FIXME: Code and description don't match for 0x6982 and 0x6983 based on 51 | // apdu spec: https://www.eftlab.co.uk/index.php/site-map/knowledge-base/118-apdu-response-list 52 | 53 | case 0x6400: 54 | return "[APDU_CODE_EXECUTION_ERROR] No information given (NV-Ram not changed)" 55 | case 0x6700: 56 | return "[APDU_CODE_WRONG_LENGTH] Wrong length" 57 | case 0x6982: 58 | return "[APDU_CODE_EMPTY_BUFFER] Security condition not satisfied" 59 | case 0x6983: 60 | return "[APDU_CODE_OUTPUT_BUFFER_TOO_SMALL] Authentication method blocked" 61 | case 0x6984: 62 | return "[APDU_CODE_DATA_INVALID] Referenced data reversibly blocked (invalidated)" 63 | case 0x6985: 64 | return "[APDU_CODE_CONDITIONS_NOT_SATISFIED] Conditions of use not satisfied" 65 | case 0x6986: 66 | return "[APDU_CODE_COMMAND_NOT_ALLOWED] Command not allowed / User Rejected (no current EF)" 67 | case 0x6A80: 68 | return "[APDU_CODE_BAD_KEY_HANDLE] The parameters in the data field are incorrect" 69 | case 0x6B00: 70 | return "[APDU_CODE_INVALID_P1P2] Wrong parameter(s) P1-P2" 71 | case 0x6D00: 72 | return "[APDU_CODE_INS_NOT_SUPPORTED] Instruction code not supported or invalid" 73 | case 0x6E00: 74 | return "[APDU_CODE_CLA_NOT_SUPPORTED] CLA not supported" 75 | case 0x6E01: 76 | return "[APDU_CODE_APP_NOT_OPEN] Ledger Connected but Chain Specific App Not Open" 77 | case 0x6F00: 78 | return "APDU_CODE_UNKNOWN" 79 | case 0x6F01: 80 | return "APDU_CODE_SIGN_VERIFY_ERROR" 81 | default: 82 | return fmt.Sprintf("APDU Error Code from Ledger Device: 0x%04x", errorCode) 83 | } 84 | } 85 | 86 | // SerializePacket serializes a command into a packet for transmission. 87 | func SerializePacket( 88 | channel uint16, 89 | command []byte, 90 | packetSize int, 91 | sequenceIdx uint16) ([]byte, int, error) { 92 | 93 | if packetSize < 3 { 94 | return nil, 0, ErrPacketSize 95 | } 96 | 97 | headerOffset := 5 98 | if sequenceIdx == 0 { 99 | headerOffset += 2 100 | } 101 | 102 | result := make([]byte, packetSize) 103 | buffer := result 104 | 105 | // Insert channel (2 bytes) 106 | codec.PutUint16(buffer, channel) 107 | 108 | // Insert tag (1 byte) 109 | buffer[2] = 0x05 110 | 111 | // Insert sequenceIdx (2 bytes) 112 | codec.PutUint16(buffer[3:], sequenceIdx) 113 | 114 | // Only insert total size of the command in the first package 115 | if sequenceIdx == 0 { 116 | commandLength := uint16(len(command)) 117 | codec.PutUint16(buffer[5:], commandLength) 118 | } 119 | 120 | offset := copy(buffer[headerOffset:], command) 121 | return result, offset, nil 122 | } 123 | 124 | // DeserializePacket deserializes a packet into its original command. 125 | func DeserializePacket( 126 | channel uint16, 127 | packet []byte, 128 | sequenceIdx uint16) ([]byte, uint16, bool, error) { 129 | 130 | const ( 131 | minFirstPacketSize = 7 132 | minPacketSize = 5 133 | tag = 0x05 134 | ) 135 | 136 | if (sequenceIdx == 0 && len(packet) < minFirstPacketSize) || (sequenceIdx > 0 && len(packet) < minPacketSize) { 137 | return nil, 0, false, errors.New("cannot deserialize the packet. header information is missing") 138 | } 139 | 140 | headerOffset := 2 141 | 142 | if codec.Uint16(packet) != channel { 143 | return nil, 0, false, fmt.Errorf("%w: expected %d, got %d", ErrInvalidChannel, channel, codec.Uint16(packet)) 144 | } 145 | 146 | if packet[headerOffset] != tag { 147 | return nil, 0, false, fmt.Errorf("invalid tag. expected %d, got %d", tag, packet[headerOffset]) 148 | } 149 | headerOffset++ 150 | 151 | foundSequenceIdx := codec.Uint16(packet[headerOffset:]) 152 | isSequenceZero := foundSequenceIdx == 0 153 | 154 | if foundSequenceIdx != sequenceIdx { 155 | return nil, 0, isSequenceZero, fmt.Errorf("wrong sequenceIdx: expected %d, got %d", sequenceIdx, foundSequenceIdx) 156 | } 157 | headerOffset += 2 158 | 159 | var totalResponseLength uint16 160 | if sequenceIdx == 0 { 161 | totalResponseLength = codec.Uint16(packet[headerOffset:]) 162 | headerOffset += 2 163 | } 164 | 165 | result := packet[headerOffset:] 166 | return result, totalResponseLength, isSequenceZero, nil 167 | } 168 | 169 | // WrapCommandAPDU turns the command into a sequence of packets of specified size. 170 | func WrapCommandAPDU( 171 | channel uint16, 172 | command []byte, 173 | packetSize int) ([]byte, error) { 174 | 175 | var totalResult []byte 176 | var sequenceIdx uint16 177 | 178 | for len(command) > 0 { 179 | packet, offset, err := SerializePacket(channel, command, packetSize, sequenceIdx) 180 | if err != nil { 181 | return nil, err 182 | } 183 | command = command[offset:] 184 | totalResult = append(totalResult, packet...) 185 | sequenceIdx++ 186 | } 187 | 188 | return totalResult, nil 189 | } 190 | 191 | // UnwrapResponseAPDU parses a response of 64 byte packets into the real data. 192 | func UnwrapResponseAPDU(channel uint16, pipe <-chan []byte, packetSize int) ([]byte, error) { 193 | var sequenceIdx uint16 194 | var totalResult []byte 195 | var totalSize uint16 196 | var foundZeroSequence bool 197 | 198 | for buffer := range pipe { 199 | result, responseSize, isSequenceZero, err := DeserializePacket(channel, buffer, sequenceIdx) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | // Recover from a known error condition: 205 | // * Discard messages left over from previous exchange until isSequenceZero == true 206 | if !foundZeroSequence && !isSequenceZero { 207 | continue 208 | } 209 | foundZeroSequence = true 210 | 211 | // Initialize totalSize 212 | if totalSize == 0 { 213 | totalSize = responseSize 214 | } 215 | 216 | totalResult = append(totalResult, result...) 217 | sequenceIdx++ 218 | 219 | if len(totalResult) >= int(totalSize) { 220 | break 221 | } 222 | } 223 | 224 | // Remove trailing zeros 225 | return totalResult[:totalSize], nil 226 | } 227 | -------------------------------------------------------------------------------- /apduWrapper_test.go: -------------------------------------------------------------------------------- 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 | "fmt" 22 | "math" 23 | "testing" 24 | "unsafe" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func Test_SerializePacket_EmptyCommand(t *testing.T) { 30 | var command = make([]byte, 1) 31 | 32 | _, _, err := SerializePacket(0x0101, command, 64, 0) 33 | assert.Nil(t, err, "Commands smaller than 3 bytes should return error") 34 | } 35 | 36 | func Test_SerializePacket_PacketSize(t *testing.T) { 37 | 38 | var packetSize = 64 39 | type header struct { 40 | channel uint16 41 | tag uint8 42 | sequenceIdx uint16 43 | commandLen uint16 44 | } 45 | 46 | h := header{channel: 0x0101, tag: 0x05, sequenceIdx: 0, commandLen: 32} 47 | 48 | var command = make([]byte, h.commandLen) 49 | 50 | result, _, _ := SerializePacket( 51 | h.channel, 52 | command, 53 | packetSize, 54 | h.sequenceIdx) 55 | 56 | assert.Equal(t, len(result), packetSize, "Packet size is wrong") 57 | } 58 | 59 | func Test_SerializePacket_Header(t *testing.T) { 60 | 61 | var packetSize = 64 62 | type header struct { 63 | channel uint16 64 | tag uint8 65 | sequenceIdx uint16 66 | commandLen uint16 67 | } 68 | 69 | h := header{channel: 0x0101, tag: 0x05, sequenceIdx: 0, commandLen: 32} 70 | 71 | var command = make([]byte, h.commandLen) 72 | 73 | result, _, _ := SerializePacket( 74 | h.channel, 75 | command, 76 | packetSize, 77 | h.sequenceIdx) 78 | 79 | assert.Equal(t, codec.Uint16(result), h.channel, "Channel not properly serialized") 80 | assert.Equal(t, result[2], h.tag, "Tag not properly serialized") 81 | assert.Equal(t, codec.Uint16(result[3:]), h.sequenceIdx, "SequenceIdx not properly serialized") 82 | assert.Equal(t, codec.Uint16(result[5:]), h.commandLen, "Command len not properly serialized") 83 | } 84 | 85 | func Test_SerializePacket_Offset(t *testing.T) { 86 | 87 | var packetSize = 64 88 | type header struct { 89 | channel uint16 90 | tag uint8 91 | sequenceIdx uint16 92 | commandLen uint16 93 | } 94 | 95 | h := header{channel: 0x0101, tag: 0x05, sequenceIdx: 0, commandLen: 100} 96 | 97 | var command = make([]byte, h.commandLen) 98 | 99 | _, offset, _ := SerializePacket( 100 | h.channel, 101 | command, 102 | packetSize, 103 | h.sequenceIdx) 104 | 105 | assert.Equal(t, packetSize-int(unsafe.Sizeof(h))+1, offset, "Wrong offset returned. Offset must point to the next command byte that needs to be packetized.") 106 | } 107 | 108 | func Test_WrapCommandAPDU_NumberOfPackets(t *testing.T) { 109 | 110 | var packetSize = 64 111 | type firstHeader struct { 112 | channel uint16 113 | sequenceIdx uint16 114 | commandLen uint16 115 | tag uint8 116 | } 117 | 118 | h1 := firstHeader{channel: 0x0101, tag: 0x05, sequenceIdx: 0, commandLen: 100} 119 | 120 | var command = make([]byte, h1.commandLen) 121 | 122 | result, _ := WrapCommandAPDU( 123 | h1.channel, 124 | command, 125 | packetSize) 126 | 127 | assert.Equal(t, packetSize*2, len(result), "Result buffer size is not correct") 128 | } 129 | 130 | func Test_WrapCommandAPDU_CheckHeaders(t *testing.T) { 131 | 132 | var packetSize = 64 133 | type firstHeader struct { 134 | channel uint16 135 | sequenceIdx uint16 136 | commandLen uint16 137 | tag uint8 138 | } 139 | 140 | h1 := firstHeader{channel: 0x0101, tag: 0x05, sequenceIdx: 0, commandLen: 100} 141 | 142 | var command = make([]byte, h1.commandLen) 143 | 144 | result, _ := WrapCommandAPDU( 145 | h1.channel, 146 | command, 147 | packetSize) 148 | 149 | assert.Equal(t, h1.channel, codec.Uint16(result), "Channel not properly serialized") 150 | assert.Equal(t, h1.tag, result[2], "Tag not properly serialized") 151 | assert.Equal(t, 0, int(codec.Uint16(result[3:])), "SequenceIdx not properly serialized") 152 | assert.Equal(t, int(h1.commandLen), int(codec.Uint16(result[5:])), "Command len not properly serialized") 153 | 154 | var offsetOfSecondPacket = packetSize 155 | assert.Equal(t, h1.channel, codec.Uint16(result[offsetOfSecondPacket:]), "Channel not properly serialized") 156 | assert.Equal(t, h1.tag, result[offsetOfSecondPacket+2], "Tag not properly serialized") 157 | assert.Equal(t, 1, int(codec.Uint16(result[offsetOfSecondPacket+3:])), "SequenceIdx not properly serialized") 158 | } 159 | 160 | func Test_WrapCommandAPDU_CheckData(t *testing.T) { 161 | 162 | var packetSize = 64 163 | type firstHeader struct { 164 | channel uint16 165 | sequenceIdx uint16 166 | commandLen uint16 167 | tag uint8 168 | } 169 | 170 | h1 := firstHeader{channel: 0x0101, tag: 0x05, sequenceIdx: 0, commandLen: 200} 171 | 172 | var command = make([]byte, h1.commandLen) 173 | 174 | for i := range command { 175 | command[i] = byte(i % 256) 176 | } 177 | 178 | result, _ := WrapCommandAPDU( 179 | h1.channel, 180 | command, 181 | packetSize) 182 | 183 | // Check data in the first packet 184 | assert.True(t, bytes.Equal(command[0:64-7], result[7:64])) 185 | 186 | result = result[64:] 187 | command = command[64-7:] 188 | // Check data in the second packet 189 | assert.True(t, bytes.Equal(command[0:64-5], result[5:64])) 190 | 191 | result = result[64:] 192 | command = command[64-5:] 193 | // Check data in the third packet 194 | assert.True(t, bytes.Equal(command[0:64-5], result[5:64])) 195 | 196 | result = result[64:] 197 | command = command[64-5:] 198 | 199 | // Check data in the last packet 200 | assert.True(t, bytes.Equal(command[0:], result[5:5+len(command)])) 201 | 202 | // The remaining bytes in the result should be zeros 203 | result = result[5+len(command):] 204 | assert.True(t, bytes.Equal(result, make([]byte, len(result)))) 205 | } 206 | 207 | func Test_DeserializePacket_FirstPacket(t *testing.T) { 208 | 209 | var sampleCommand = []byte{'H', 'e', 'l', 'l', 'o', 0} 210 | 211 | var packetSize = 64 212 | var firstPacketHeaderSize = 7 213 | packet, _, _ := SerializePacket(0x0101, sampleCommand, packetSize, 0) 214 | 215 | output, totalSize, isSequenceZero, err := DeserializePacket(0x0101, packet, 0) 216 | 217 | assert.Nil(t, err, "Simple deserialize should not have errors") 218 | assert.Equal(t, len(sampleCommand), int(totalSize), "TotalSize is incorrect") 219 | assert.Equal(t, packetSize-firstPacketHeaderSize, len(output), "Size of the deserialized packet is wrong") 220 | assert.Equal(t, true, isSequenceZero, "Test Case Should Find Sequence == 0") 221 | assert.True(t, bytes.Equal(output[:len(sampleCommand)], sampleCommand), "Deserialized message does not match the original") 222 | } 223 | 224 | func Test_DeserializePacket_SecondMessage(t *testing.T) { 225 | var sampleCommand = []byte{'H', 'e', 'l', 'l', 'o', 0} 226 | 227 | var packetSize = 64 228 | var firstPacketHeaderSize = 5 // second packet does not have responseLength (uint16) in the header 229 | packet, _, _ := SerializePacket(0x0101, sampleCommand, packetSize, 1) 230 | 231 | output, totalSize, isSequenceZero, err := DeserializePacket(0x0101, packet, 1) 232 | 233 | assert.Nil(t, err, "Simple deserialize should not have errors") 234 | assert.Equal(t, 0, int(totalSize), "TotalSize should not be returned from deserialization of non-first packet") 235 | assert.Equal(t, packetSize-firstPacketHeaderSize, len(output), "Size of the deserialized packet is wrong") 236 | assert.Equal(t, false, isSequenceZero, "Test Case Should Find Sequence == 1") 237 | assert.True(t, bytes.Equal(output[:len(sampleCommand)], sampleCommand), "Deserialized message does not match the original") 238 | } 239 | 240 | func Test_UnwrapApdu_SmokeTest(t *testing.T) { 241 | const channel uint16 = 0x8002 242 | 243 | inputSize := 200 244 | var packetSize = 64 245 | 246 | // Initialize some dummy input 247 | var input = make([]byte, inputSize) 248 | for i := range input { 249 | input[i] = byte(i % 256) 250 | } 251 | 252 | serialized, _ := WrapCommandAPDU(channel, input, packetSize) 253 | 254 | // Allocate enough packets 255 | pipe := make(chan []byte, int(math.Ceil(float64(inputSize)/float64(packetSize)))) 256 | // Send all the packets to the pipe 257 | for len(serialized) > 0 { 258 | pipe <- serialized[:packetSize] 259 | serialized = serialized[packetSize:] 260 | } 261 | 262 | output, _ := UnwrapResponseAPDU(channel, pipe, packetSize) 263 | 264 | fmt.Printf("INPUT : %x\n", input) 265 | fmt.Printf("SERIALIZED: %x\n", serialized) 266 | fmt.Printf("OUTPUT : %x\n", output) 267 | 268 | assert.Equal(t, len(input), len(output), "Input and output messages have different size") 269 | assert.True(t, 270 | bytes.Equal(input, output), 271 | "Input message does not match message which was serialized and then deserialized") 272 | } 273 | 274 | func TestSerializePacketWithInvalidSize(t *testing.T) { 275 | _, _, err := SerializePacket(0x0101, []byte{1, 2}, 2, 0) 276 | assert.ErrorIs(t, err, ErrPacketSize) 277 | } 278 | 279 | func TestDeserializePacketWithInvalidChannel(t *testing.T) { 280 | packet := []byte{0x02, 0x02, 0x05, 0x00, 0x00, 0x00, 0x20} 281 | _, _, _, err := DeserializePacket(0x0101, packet, 0) 282 | assert.ErrorIs(t, err, ErrInvalidChannel) 283 | } 284 | 285 | func TestSerializeDeserialize(t *testing.T) { 286 | sampleCommand := []byte{0x01, 0x02, 0x03, 0x04, 0x05} 287 | channel := uint16(0x0101) 288 | packetSize := 64 289 | sequenceIdx := uint16(0) 290 | 291 | packet, _, err := SerializePacket(channel, sampleCommand, packetSize, sequenceIdx) 292 | assert.NoError(t, err) 293 | 294 | output, _, _, err := DeserializePacket(channel, packet, sequenceIdx) 295 | assert.NoError(t, err) 296 | 297 | assert.True(t, bytes.Equal(output[:len(sampleCommand)], sampleCommand), "Deserialized message does not match the original") 298 | } 299 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zondax/ledger-go 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.4 7 | github.com/pkg/errors v0.9.1 8 | github.com/stretchr/testify v1.9.0 9 | github.com/zondax/hid v0.9.2 10 | google.golang.org/grpc v1.66.0 11 | google.golang.org/protobuf v1.34.2 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/kr/pretty v0.3.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | github.com/rogpeppe/go-internal v1.12.0 // indirect 19 | golang.org/x/net v0.28.0 // indirect 20 | golang.org/x/sys v0.24.0 // indirect 21 | golang.org/x/text v0.17.0 // indirect 22 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect 23 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 5 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 9 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 10 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 16 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 17 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 21 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 22 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 23 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 24 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 25 | github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= 26 | github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= 27 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 28 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 29 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 30 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 31 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 32 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 33 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= 34 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 35 | google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= 36 | google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= 37 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 38 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 41 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /ledger.go: -------------------------------------------------------------------------------- 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 | // LedgerAdmin defines the interface for managing Ledger devices. 20 | type LedgerAdmin interface { 21 | CountDevices() int 22 | ListDevices() ([]string, error) 23 | Connect(deviceIndex int) (LedgerDevice, error) 24 | } 25 | 26 | // LedgerDevice defines the interface for interacting with a Ledger device. 27 | type LedgerDevice interface { 28 | Exchange(command []byte) ([]byte, error) 29 | Close() error 30 | } 31 | -------------------------------------------------------------------------------- /ledger_hid.go: -------------------------------------------------------------------------------- 1 | //go:build !ledger_mock && !ledger_zemu 2 | // +build !ledger_mock,!ledger_zemu 3 | 4 | /******************************************************************************* 5 | * (c) Zondax AG 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | ********************************************************************************/ 19 | 20 | package ledger_go 21 | 22 | import ( 23 | "errors" 24 | "fmt" 25 | "log" 26 | "sync" 27 | "time" 28 | 29 | "github.com/zondax/hid" 30 | ) 31 | 32 | const ( 33 | VendorLedger = 0x2c97 34 | UsagePageLedgerNanoS = 0xffa0 35 | Channel = 0x0101 36 | PacketSize = 64 37 | ) 38 | 39 | type LedgerAdminHID struct{} 40 | 41 | type LedgerDeviceHID struct { 42 | device *hid.Device 43 | readCo *sync.Once 44 | readChannel chan []byte 45 | } 46 | 47 | // list of supported product ids as well as their corresponding interfaces 48 | // based on https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/devices/src/index.ts 49 | var supportedLedgerProductID = map[uint8]int{ 50 | 0x40: 0, // Ledger Nano X 51 | 0x10: 0, // Ledger Nano S 52 | 0x50: 0, // Ledger Nano S Plus 53 | 0x60: 0, // Ledger Stax 54 | 0x70: 0, // Ledger Flex 55 | } 56 | 57 | func NewLedgerAdmin() LedgerAdmin { 58 | return &LedgerAdminHID{} 59 | } 60 | 61 | func (admin *LedgerAdminHID) ListDevices() ([]string, error) { 62 | devices := hid.Enumerate(0, 0) 63 | if len(devices) == 0 { 64 | log.Println("No devices. Ledger LOCKED OR Other Program/Web Browser may have control of device.") 65 | } 66 | 67 | for _, d := range devices { 68 | logDeviceInfo(d) 69 | } 70 | 71 | return []string{}, nil 72 | } 73 | 74 | func logDeviceInfo(d hid.DeviceInfo) { 75 | log.Printf("============ %s\n", d.Path) 76 | log.Printf("VendorID : %x\n", d.VendorID) 77 | log.Printf("ProductID : %x\n", d.ProductID) 78 | log.Printf("Release : %x\n", d.Release) 79 | log.Printf("Serial : %x\n", d.Serial) 80 | log.Printf("Manufacturer : %s\n", d.Manufacturer) 81 | log.Printf("Product : %s\n", d.Product) 82 | log.Printf("UsagePage : %x\n", d.UsagePage) 83 | log.Printf("Usage : %x\n", d.Usage) 84 | log.Printf("\n") 85 | } 86 | 87 | func isLedgerDevice(d hid.DeviceInfo) bool { 88 | deviceFound := d.UsagePage == UsagePageLedgerNanoS 89 | 90 | // Workarounds for possible empty usage pages 91 | productIDMM := uint8(d.ProductID >> 8) 92 | if interfaceID, supported := supportedLedgerProductID[productIDMM]; deviceFound || (supported && (interfaceID == d.Interface)) { 93 | return true 94 | } 95 | 96 | return false 97 | } 98 | 99 | func (admin *LedgerAdminHID) CountDevices() int { 100 | devices := hid.Enumerate(0, 0) 101 | 102 | count := 0 103 | for _, d := range devices { 104 | if isLedgerDevice(d) { 105 | count++ 106 | } 107 | } 108 | 109 | return count 110 | } 111 | 112 | func newDevice(dev *hid.Device) *LedgerDeviceHID { 113 | return &LedgerDeviceHID{ 114 | device: dev, 115 | readCo: new(sync.Once), 116 | readChannel: make(chan []byte), 117 | } 118 | } 119 | 120 | func (admin *LedgerAdminHID) Connect(requiredIndex int) (LedgerDevice, error) { 121 | devices := hid.Enumerate(VendorLedger, 0) 122 | 123 | currentIndex := 0 124 | for _, d := range devices { 125 | if isLedgerDevice(d) { 126 | if currentIndex == requiredIndex { 127 | device, err := d.Open() 128 | if err != nil { 129 | return nil, err 130 | } 131 | deviceHID := newDevice(device) 132 | return deviceHID, nil 133 | } 134 | currentIndex++ 135 | if currentIndex > requiredIndex { 136 | break 137 | } 138 | } 139 | } 140 | 141 | return nil, fmt.Errorf("LedgerHID device (idx %d) not found: device may be locked or in use by another application", requiredIndex) 142 | } 143 | 144 | func (ledger *LedgerDeviceHID) write(buffer []byte) (int, error) { 145 | totalBytes := len(buffer) 146 | totalWrittenBytes := 0 147 | for totalBytes > totalWrittenBytes { 148 | writtenBytes, err := ledger.device.Write(buffer) 149 | 150 | if err != nil { 151 | return totalWrittenBytes, err 152 | } 153 | buffer = buffer[writtenBytes:] 154 | totalWrittenBytes += writtenBytes 155 | } 156 | return totalWrittenBytes, nil 157 | } 158 | 159 | func (ledger *LedgerDeviceHID) Read() <-chan []byte { 160 | ledger.readCo.Do(ledger.initReadChannel) 161 | return ledger.readChannel 162 | } 163 | 164 | func (ledger *LedgerDeviceHID) initReadChannel() { 165 | ledger.readChannel = make(chan []byte, 30) 166 | go ledger.readThread() 167 | } 168 | 169 | func (ledger *LedgerDeviceHID) readThread() { 170 | defer close(ledger.readChannel) 171 | 172 | for { 173 | buffer := make([]byte, PacketSize) 174 | readBytes, err := ledger.device.Read(buffer) 175 | 176 | // Check for HID Read Error (May occur even during normal runtime) 177 | if err != nil { 178 | continue 179 | } 180 | 181 | // Discard all zero packets from Ledger Nano X on macOS 182 | allZeros := true 183 | for i := 0; i < len(buffer); i++ { 184 | if buffer[i] != 0 { 185 | allZeros = false 186 | break 187 | } 188 | } 189 | 190 | // Discard all zero packet 191 | if allZeros { 192 | // HID Returned Empty Packet - Retry Read 193 | continue 194 | } 195 | 196 | select { 197 | case ledger.readChannel <- buffer[:readBytes]: 198 | // Send data to UnwrapResponseAPDU 199 | default: 200 | // Possible source of bugs 201 | // Drop a buffer if ledger.readChannel is busy 202 | } 203 | } 204 | } 205 | 206 | func (ledger *LedgerDeviceHID) drainRead() { 207 | // Allow time for late packet arrivals (When main program doesn't read enough packets) 208 | <-time.After(50 * time.Millisecond) 209 | for { 210 | select { 211 | case <-ledger.readChannel: 212 | default: 213 | return 214 | } 215 | } 216 | } 217 | 218 | func (ledger *LedgerDeviceHID) Exchange(command []byte) ([]byte, error) { 219 | log.Printf("Sending command: %X", command) 220 | // Purge messages that arrived after previous exchange completed 221 | ledger.drainRead() 222 | 223 | if len(command) < 5 { 224 | return nil, fmt.Errorf("APDU commands should not be smaller than 5") 225 | } 226 | 227 | if (byte)(len(command)-5) != command[4] { 228 | return nil, fmt.Errorf("APDU[data length] mismatch") 229 | } 230 | 231 | serializedCommand, err := WrapCommandAPDU(Channel, command, PacketSize) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | // Write all the packets 237 | _, err = ledger.write(serializedCommand) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | readChannel := ledger.Read() 243 | 244 | response, err := UnwrapResponseAPDU(Channel, readChannel, PacketSize) 245 | if err != nil { 246 | return nil, err 247 | } 248 | 249 | if len(response) < 2 { 250 | return nil, fmt.Errorf("len(response) < 2") 251 | } 252 | 253 | swOffset := len(response) - 2 254 | sw := codec.Uint16(response[swOffset:]) 255 | 256 | if sw != 0x9000 { 257 | return response[:swOffset], errors.New(ErrorMessage(sw)) 258 | } 259 | 260 | log.Printf("Received response: %X", response) 261 | return response[:swOffset], nil 262 | } 263 | 264 | func (ledger *LedgerDeviceHID) Close() error { 265 | return ledger.device.Close() 266 | } 267 | -------------------------------------------------------------------------------- /ledger_mock.go: -------------------------------------------------------------------------------- 1 | //go:build ledger_mock 2 | // +build ledger_mock 3 | 4 | /******************************************************************************* 5 | * (c) Zondax AG 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | ********************************************************************************/ 19 | 20 | package ledger_go 21 | 22 | import ( 23 | "encoding/hex" 24 | "fmt" 25 | ) 26 | 27 | const mockDeviceName = "Mock device" 28 | 29 | type LedgerAdminMock struct{} 30 | 31 | type LedgerDeviceMock struct { 32 | commands map[string]string 33 | } 34 | 35 | func NewLedgerAdmin() LedgerAdmin { 36 | return &LedgerAdminMock{} 37 | } 38 | 39 | func (admin *LedgerAdminMock) ListDevices() ([]string, error) { 40 | return []string{mockDeviceName}, nil 41 | } 42 | 43 | func (admin *LedgerAdminMock) CountDevices() int { 44 | return 1 45 | } 46 | 47 | func (admin *LedgerAdminMock) Connect(deviceIndex int) (LedgerDevice, error) { 48 | return NewLedgerDeviceMock(), nil 49 | } 50 | 51 | func NewLedgerDeviceMock() *LedgerDeviceMock { 52 | return &LedgerDeviceMock{ 53 | commands: make(map[string]string), 54 | } 55 | } 56 | 57 | func (ledger *LedgerDeviceMock) Exchange(command []byte) ([]byte, error) { 58 | hexCommand := hex.EncodeToString(command) 59 | if reply, ok := ledger.commands[hexCommand]; ok { 60 | return hex.DecodeString(reply) 61 | } 62 | return nil, fmt.Errorf("unknown command: %s", hexCommand) 63 | } 64 | 65 | func (ledger *LedgerDeviceMock) SetCommandReplies(commands map[string]string) { 66 | ledger.commands = commands 67 | } 68 | 69 | func (ledger *LedgerDeviceMock) ClearCommands() { 70 | ledger.commands = make(map[string]string) 71 | } 72 | 73 | func (ledger *LedgerDeviceMock) Close() error { 74 | // Nothing to do here 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /ledger_test.go: -------------------------------------------------------------------------------- 1 | //go:build ledger_mock 2 | // +build ledger_mock 3 | 4 | /******************************************************************************* 5 | * (c) Zondax AG 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | ********************************************************************************/ 19 | 20 | package ledger_go 21 | 22 | import ( 23 | "fmt" 24 | "sync" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | var mux sync.Mutex 32 | 33 | func TestLedger(t *testing.T) { 34 | tests := []struct { 35 | name string 36 | test func(t *testing.T) 37 | }{ 38 | {"CountLedgerDevices", Test_CountLedgerDevices}, 39 | {"ListDevices", TestListDevices}, 40 | {"GetLedger", Test_GetLedger}, 41 | {"BasicExchange", Test_BasicExchange}, 42 | {"Connect", TestConnect}, 43 | {"GetVersion", TestGetVersion}, 44 | } 45 | 46 | for _, tt := range tests { 47 | t.Run(tt.name, tt.test) 48 | } 49 | } 50 | 51 | func Test_CountLedgerDevices(t *testing.T) { 52 | mux.Lock() 53 | defer mux.Unlock() 54 | 55 | ledgerAdmin := NewLedgerAdmin() 56 | count := ledgerAdmin.CountDevices() 57 | require.True(t, count > 0) 58 | } 59 | 60 | func TestListDevices(t *testing.T) { 61 | ledgerAdmin := NewLedgerAdmin() 62 | 63 | devices, err := ledgerAdmin.ListDevices() 64 | if err != nil { 65 | t.Fatalf("Error listing devices: %v", err) 66 | } 67 | assert.NotNil(t, devices, "Devices should not be nil") 68 | } 69 | 70 | func Test_GetLedger(t *testing.T) { 71 | mux.Lock() 72 | defer mux.Unlock() 73 | 74 | ledgerAdmin := NewLedgerAdmin() 75 | count := ledgerAdmin.CountDevices() 76 | require.True(t, count > 0) 77 | 78 | ledger, err := ledgerAdmin.Connect(0) 79 | if err != nil { 80 | t.Fatalf("Error connecting to ledger: %v", err) 81 | } 82 | defer ledger.Close() 83 | 84 | assert.NoError(t, err) 85 | assert.NotNil(t, ledger) 86 | } 87 | 88 | func Test_BasicExchange(t *testing.T) { 89 | mux.Lock() 90 | defer mux.Unlock() 91 | 92 | ledgerAdmin := NewLedgerAdmin() 93 | count := ledgerAdmin.CountDevices() 94 | require.True(t, count > 0) 95 | 96 | ledger, err := ledgerAdmin.Connect(0) 97 | if err != nil { 98 | t.Fatalf("Error connecting to ledger: %v", err) 99 | } 100 | defer ledger.Close() 101 | 102 | // Set expected replies for the commands (only if using mock) 103 | if mockLedger, ok := ledger.(*LedgerDeviceMock); ok { 104 | mockLedger.SetCommandReplies(map[string]string{ 105 | "e001000000": "311000040853706563756c6f73000b53706563756c6f734d4355", 106 | }) 107 | } 108 | 109 | // Call device info (this should work in main menu and many apps) 110 | message := []byte{0xE0, 0x01, 0, 0, 0} 111 | 112 | for i := 0; i < 10; i++ { 113 | response, err := ledger.Exchange(message) 114 | 115 | if err != nil { 116 | fmt.Printf("iteration %d\n", i) 117 | t.Fatalf("Error: %s", err.Error()) 118 | } 119 | 120 | require.True(t, len(response) > 0) 121 | } 122 | } 123 | 124 | func TestConnect(t *testing.T) { 125 | ledgerAdmin := NewLedgerAdmin() 126 | 127 | ledger, err := ledgerAdmin.Connect(0) 128 | if err != nil { 129 | t.Fatalf("Error connecting to ledger: %v", err) 130 | } 131 | defer ledger.Close() 132 | 133 | assert.NotNil(t, ledger, "Ledger should not be nil") 134 | } 135 | 136 | func TestGetVersion(t *testing.T) { 137 | ledgerAdmin := NewLedgerAdmin() 138 | 139 | ledger, err := ledgerAdmin.Connect(0) 140 | if err != nil { 141 | t.Fatalf("Error connecting to ledger: %v", err) 142 | } 143 | defer ledger.Close() 144 | 145 | // Set expected replies for the commands (only if using mock) 146 | if mockLedger, ok := ledger.(*LedgerDeviceMock); ok { 147 | mockLedger.SetCommandReplies(map[string]string{ 148 | "e001000000": "311000040853706563756c6f73000b53706563756c6f734d4355", 149 | }) 150 | } 151 | 152 | // Call device info (this should work in main menu and many apps) 153 | message := []byte{0xE0, 0x01, 0, 0, 0} 154 | 155 | response, err := ledger.Exchange(message) 156 | assert.NoError(t, err) 157 | assert.NotEmpty(t, response, "Response should not be empty") 158 | } 159 | -------------------------------------------------------------------------------- /ledger_zemu.go: -------------------------------------------------------------------------------- 1 | //go:build ledger_zemu 2 | // +build ledger_zemu 3 | 4 | /******************************************************************************* 5 | * (c) Zondax AG 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | ********************************************************************************/ 19 | 20 | package ledger_go 21 | 22 | import ( 23 | "context" 24 | "errors" 25 | "fmt" 26 | 27 | "google.golang.org/grpc" 28 | ) 29 | 30 | const ( 31 | defaultGrpcURL = "localhost" 32 | defaultGrpcPort = "3002" 33 | ) 34 | 35 | type LedgerAdminZemu struct { 36 | grpcURL string 37 | grpcPort string 38 | } 39 | 40 | type LedgerDeviceZemu struct { 41 | connection *grpc.ClientConn 42 | client ZemuCommandClient 43 | } 44 | 45 | func NewLedgerAdmin() *LedgerAdminZemu { 46 | return &LedgerAdminZemu{ 47 | grpcURL: defaultGrpcURL, 48 | grpcPort: defaultGrpcPort, 49 | } 50 | } 51 | 52 | func (admin *LedgerAdminZemu) ListDevices() ([]string, error) { 53 | // It does not make sense for zemu devices 54 | x := []string{"Zemu device"} 55 | return x, nil 56 | } 57 | 58 | func (admin *LedgerAdminZemu) CountDevices() int { 59 | // TODO: Always 1, maybe zero if zemu has not elf?? 60 | return 1 61 | } 62 | 63 | func (admin *LedgerAdminZemu) Connect(deviceIndex int) (*LedgerDeviceZemu, error) { 64 | serverAddr := admin.grpcURL + ":" + admin.grpcPort 65 | //TODO: check Dial flags 66 | conn, err := grpc.Dial(serverAddr, grpc.WithInsecure()) 67 | 68 | if err != nil { 69 | err = fmt.Errorf("could not connect to rpc server at %q : %q", serverAddr, err) 70 | return &LedgerDeviceZemu{}, err 71 | } 72 | 73 | client := NewZemuCommandClient(conn) 74 | 75 | return &LedgerDeviceZemu{connection: conn, client: client}, nil 76 | } 77 | 78 | func (ledger *LedgerDeviceZemu) Exchange(command []byte) ([]byte, error) { 79 | 80 | if len(command) < 5 { 81 | return nil, fmt.Errorf("APDU commands should not be smaller than 5") 82 | } 83 | 84 | if (byte)(len(command)-5) != command[4] { 85 | return nil, fmt.Errorf("APDU[data length] mismatch") 86 | } 87 | 88 | // Send to Zemu and return reply or error 89 | r, err := ledger.client.Exchange(context.Background(), &ExchangeRequest{Command: command}) 90 | 91 | if err != nil { 92 | err = fmt.Errorf("could not call rpc service: %q", err) 93 | return []byte{}, err 94 | } 95 | 96 | response := r.Reply 97 | 98 | if len(response) < 2 { 99 | return nil, fmt.Errorf("len(response) < 2") 100 | } 101 | 102 | swOffset := len(response) - 2 103 | sw := codec.Uint16(response[swOffset:]) 104 | 105 | if sw != 0x9000 { 106 | return response[:swOffset], errors.New(ErrorMessage(sw)) 107 | } 108 | 109 | return response[:swOffset], nil 110 | } 111 | 112 | func (ledger *LedgerDeviceZemu) Close() error { 113 | err := ledger.connection.Close() 114 | 115 | if err != nil { 116 | err = fmt.Errorf("could not close connection to rpc server") 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /zemu.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.23.0 4 | // protoc (unknown) 5 | // source: zemu.proto 6 | 7 | package ledger_go 8 | 9 | import ( 10 | context "context" 11 | proto "github.com/golang/protobuf/proto" 12 | grpc "google.golang.org/grpc" 13 | codes "google.golang.org/grpc/codes" 14 | status "google.golang.org/grpc/status" 15 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 16 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 17 | reflect "reflect" 18 | sync "sync" 19 | ) 20 | 21 | const ( 22 | // Verify that this generated code is sufficiently up-to-date. 23 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 24 | // Verify that runtime/protoimpl is sufficiently up-to-date. 25 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 26 | ) 27 | 28 | // This is a compile-time assertion that a sufficiently up-to-date version 29 | // of the legacy proto package is being used. 30 | const _ = proto.ProtoPackageIsVersion4 31 | 32 | type ExchangeRequest struct { 33 | state protoimpl.MessageState 34 | sizeCache protoimpl.SizeCache 35 | unknownFields protoimpl.UnknownFields 36 | 37 | Command []byte `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` 38 | } 39 | 40 | func (x *ExchangeRequest) Reset() { 41 | *x = ExchangeRequest{} 42 | if protoimpl.UnsafeEnabled { 43 | mi := &file_zemu_proto_msgTypes[0] 44 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 45 | ms.StoreMessageInfo(mi) 46 | } 47 | } 48 | 49 | func (x *ExchangeRequest) String() string { 50 | return protoimpl.X.MessageStringOf(x) 51 | } 52 | 53 | func (*ExchangeRequest) ProtoMessage() {} 54 | 55 | func (x *ExchangeRequest) ProtoReflect() protoreflect.Message { 56 | mi := &file_zemu_proto_msgTypes[0] 57 | if protoimpl.UnsafeEnabled && x != nil { 58 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 59 | if ms.LoadMessageInfo() == nil { 60 | ms.StoreMessageInfo(mi) 61 | } 62 | return ms 63 | } 64 | return mi.MessageOf(x) 65 | } 66 | 67 | // Deprecated: Use ExchangeRequest.ProtoReflect.Descriptor instead. 68 | func (*ExchangeRequest) Descriptor() ([]byte, []int) { 69 | return file_zemu_proto_rawDescGZIP(), []int{0} 70 | } 71 | 72 | func (x *ExchangeRequest) GetCommand() []byte { 73 | if x != nil { 74 | return x.Command 75 | } 76 | return nil 77 | } 78 | 79 | type ExchangeReply struct { 80 | state protoimpl.MessageState 81 | sizeCache protoimpl.SizeCache 82 | unknownFields protoimpl.UnknownFields 83 | 84 | Reply []byte `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"` 85 | } 86 | 87 | func (x *ExchangeReply) Reset() { 88 | *x = ExchangeReply{} 89 | if protoimpl.UnsafeEnabled { 90 | mi := &file_zemu_proto_msgTypes[1] 91 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 92 | ms.StoreMessageInfo(mi) 93 | } 94 | } 95 | 96 | func (x *ExchangeReply) String() string { 97 | return protoimpl.X.MessageStringOf(x) 98 | } 99 | 100 | func (*ExchangeReply) ProtoMessage() {} 101 | 102 | func (x *ExchangeReply) ProtoReflect() protoreflect.Message { 103 | mi := &file_zemu_proto_msgTypes[1] 104 | if protoimpl.UnsafeEnabled && x != nil { 105 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 106 | if ms.LoadMessageInfo() == nil { 107 | ms.StoreMessageInfo(mi) 108 | } 109 | return ms 110 | } 111 | return mi.MessageOf(x) 112 | } 113 | 114 | // Deprecated: Use ExchangeReply.ProtoReflect.Descriptor instead. 115 | func (*ExchangeReply) Descriptor() ([]byte, []int) { 116 | return file_zemu_proto_rawDescGZIP(), []int{1} 117 | } 118 | 119 | func (x *ExchangeReply) GetReply() []byte { 120 | if x != nil { 121 | return x.Reply 122 | } 123 | return nil 124 | } 125 | 126 | var File_zemu_proto protoreflect.FileDescriptor 127 | 128 | var file_zemu_proto_rawDesc = []byte{ 129 | 0x0a, 0x0a, 0x7a, 0x65, 0x6d, 0x75, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x6c, 0x65, 130 | 0x64, 0x67, 0x65, 0x72, 0x5f, 0x67, 0x6f, 0x22, 0x2b, 0x0a, 0x0f, 0x45, 0x78, 0x63, 0x68, 0x61, 131 | 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 132 | 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6d, 133 | 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x25, 0x0a, 0x0d, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 134 | 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x18, 0x01, 135 | 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x32, 0x51, 0x0a, 0x0b, 0x5a, 136 | 0x65, 0x6d, 0x75, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x42, 0x0a, 0x08, 0x45, 0x78, 137 | 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x2e, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x5f, 138 | 0x67, 0x6f, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 139 | 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x5f, 0x67, 0x6f, 0x2e, 0x45, 140 | 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x62, 0x06, 141 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 142 | } 143 | 144 | var ( 145 | file_zemu_proto_rawDescOnce sync.Once 146 | file_zemu_proto_rawDescData = file_zemu_proto_rawDesc 147 | ) 148 | 149 | func file_zemu_proto_rawDescGZIP() []byte { 150 | file_zemu_proto_rawDescOnce.Do(func() { 151 | file_zemu_proto_rawDescData = protoimpl.X.CompressGZIP(file_zemu_proto_rawDescData) 152 | }) 153 | return file_zemu_proto_rawDescData 154 | } 155 | 156 | var file_zemu_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 157 | var file_zemu_proto_goTypes = []interface{}{ 158 | (*ExchangeRequest)(nil), // 0: ledger_go.ExchangeRequest 159 | (*ExchangeReply)(nil), // 1: ledger_go.ExchangeReply 160 | } 161 | var file_zemu_proto_depIdxs = []int32{ 162 | 0, // 0: ledger_go.ZemuCommand.Exchange:input_type -> ledger_go.ExchangeRequest 163 | 1, // 1: ledger_go.ZemuCommand.Exchange:output_type -> ledger_go.ExchangeReply 164 | 1, // [1:2] is the sub-list for method output_type 165 | 0, // [0:1] is the sub-list for method input_type 166 | 0, // [0:0] is the sub-list for extension type_name 167 | 0, // [0:0] is the sub-list for extension extendee 168 | 0, // [0:0] is the sub-list for field type_name 169 | } 170 | 171 | func init() { file_zemu_proto_init() } 172 | func file_zemu_proto_init() { 173 | if File_zemu_proto != nil { 174 | return 175 | } 176 | if !protoimpl.UnsafeEnabled { 177 | file_zemu_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 178 | switch v := v.(*ExchangeRequest); i { 179 | case 0: 180 | return &v.state 181 | case 1: 182 | return &v.sizeCache 183 | case 2: 184 | return &v.unknownFields 185 | default: 186 | return nil 187 | } 188 | } 189 | file_zemu_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 190 | switch v := v.(*ExchangeReply); i { 191 | case 0: 192 | return &v.state 193 | case 1: 194 | return &v.sizeCache 195 | case 2: 196 | return &v.unknownFields 197 | default: 198 | return nil 199 | } 200 | } 201 | } 202 | type x struct{} 203 | out := protoimpl.TypeBuilder{ 204 | File: protoimpl.DescBuilder{ 205 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 206 | RawDescriptor: file_zemu_proto_rawDesc, 207 | NumEnums: 0, 208 | NumMessages: 2, 209 | NumExtensions: 0, 210 | NumServices: 1, 211 | }, 212 | GoTypes: file_zemu_proto_goTypes, 213 | DependencyIndexes: file_zemu_proto_depIdxs, 214 | MessageInfos: file_zemu_proto_msgTypes, 215 | }.Build() 216 | File_zemu_proto = out.File 217 | file_zemu_proto_rawDesc = nil 218 | file_zemu_proto_goTypes = nil 219 | file_zemu_proto_depIdxs = nil 220 | } 221 | 222 | // Reference imports to suppress errors if they are not otherwise used. 223 | var _ context.Context 224 | var _ grpc.ClientConnInterface 225 | 226 | // This is a compile-time assertion to ensure that this generated file 227 | // is compatible with the grpc package it is being compiled against. 228 | const _ = grpc.SupportPackageIsVersion6 229 | 230 | // ZemuCommandClient is the client API for ZemuCommand service. 231 | // 232 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 233 | type ZemuCommandClient interface { 234 | Exchange(ctx context.Context, in *ExchangeRequest, opts ...grpc.CallOption) (*ExchangeReply, error) 235 | } 236 | 237 | type zemuCommandClient struct { 238 | cc grpc.ClientConnInterface 239 | } 240 | 241 | func NewZemuCommandClient(cc grpc.ClientConnInterface) ZemuCommandClient { 242 | return &zemuCommandClient{cc} 243 | } 244 | 245 | func (c *zemuCommandClient) Exchange(ctx context.Context, in *ExchangeRequest, opts ...grpc.CallOption) (*ExchangeReply, error) { 246 | out := new(ExchangeReply) 247 | err := c.cc.Invoke(ctx, "/ledger_go.ZemuCommand/Exchange", in, out, opts...) 248 | if err != nil { 249 | return nil, err 250 | } 251 | return out, nil 252 | } 253 | 254 | // ZemuCommandServer is the server API for ZemuCommand service. 255 | type ZemuCommandServer interface { 256 | Exchange(context.Context, *ExchangeRequest) (*ExchangeReply, error) 257 | } 258 | 259 | // UnimplementedZemuCommandServer can be embedded to have forward compatible implementations. 260 | type UnimplementedZemuCommandServer struct { 261 | } 262 | 263 | func (*UnimplementedZemuCommandServer) Exchange(context.Context, *ExchangeRequest) (*ExchangeReply, error) { 264 | return nil, status.Errorf(codes.Unimplemented, "method Exchange not implemented") 265 | } 266 | 267 | func RegisterZemuCommandServer(s *grpc.Server, srv ZemuCommandServer) { 268 | s.RegisterService(&_ZemuCommand_serviceDesc, srv) 269 | } 270 | 271 | func _ZemuCommand_Exchange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 272 | in := new(ExchangeRequest) 273 | if err := dec(in); err != nil { 274 | return nil, err 275 | } 276 | if interceptor == nil { 277 | return srv.(ZemuCommandServer).Exchange(ctx, in) 278 | } 279 | info := &grpc.UnaryServerInfo{ 280 | Server: srv, 281 | FullMethod: "/ledger_go.ZemuCommand/Exchange", 282 | } 283 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 284 | return srv.(ZemuCommandServer).Exchange(ctx, req.(*ExchangeRequest)) 285 | } 286 | return interceptor(ctx, in, info, handler) 287 | } 288 | 289 | var _ZemuCommand_serviceDesc = grpc.ServiceDesc{ 290 | ServiceName: "ledger_go.ZemuCommand", 291 | HandlerType: (*ZemuCommandServer)(nil), 292 | Methods: []grpc.MethodDesc{ 293 | { 294 | MethodName: "Exchange", 295 | Handler: _ZemuCommand_Exchange_Handler, 296 | }, 297 | }, 298 | Streams: []grpc.StreamDesc{}, 299 | Metadata: "zemu.proto", 300 | } 301 | -------------------------------------------------------------------------------- /zemu.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | //This file needs to be synced with the one of same name in Zemu repo 4 | 5 | package ledger_go; 6 | 7 | service ZemuCommand { 8 | rpc Exchange (ExchangeRequest) returns (ExchangeReply) {} 9 | } 10 | 11 | message ExchangeRequest { 12 | bytes command = 1; 13 | } 14 | 15 | message ExchangeReply { 16 | bytes reply = 1; 17 | } 18 | --------------------------------------------------------------------------------