├── .editorconfig ├── .githooks └── pre-commit ├── .github └── workflows │ └── go.yml ├── .gitignore ├── .gitlab-ci.yml ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── client.go ├── client_test.go ├── cmd ├── e4client │ ├── README.md │ ├── commands │ │ └── commands.go │ ├── e4client.go │ └── logger │ │ └── logger.go └── e4keygen │ ├── README.md │ └── e4keygen.go ├── commands.go ├── commands_test.go ├── crypto ├── const.go ├── crypto.go ├── crypto_test.go ├── hash.go ├── hash_test.go ├── validators.go └── validators_test.go ├── example_test.go ├── go.mod ├── go.sum ├── keys ├── json.go ├── json_test.go ├── publickey.go ├── publickey_test.go ├── symmetric.go ├── symmetric_test.go └── types.go ├── logo.png ├── scripts ├── android_bindings.sh ├── build.sh ├── devinit.sh └── unittest.sh ├── storage.go ├── storage_test.go └── test └── data └── .gitkeep /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | charset = utf-8 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | 8 | [{Makefile, *,mk}] 9 | indent_style = tab 10 | 11 | [*.go] 12 | indent_style = tab 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo 4 | count=0 5 | replacedirective=0 6 | 7 | echo "Checking for merge conflicts" 8 | for f in `git diff HEAD --name-only --diff-filter=M`; do 9 | if [[ -f $f ]] && [[ $f != .githooks/* ]]; then 10 | if [[ $(grep -c "<<<<<<" $f) -gt 0 ]] || [[ $(grep -c ">>>>>>" $f) -gt 0 ]]; then 11 | echo "$(tput setaf 1)! $f$(tput sgr0)" 12 | ((count += 1)) 13 | fi 14 | fi 15 | done 16 | 17 | if [ $count != 0 ]; then 18 | echo "$(tput setaf 1)$count$(tput sgr0) files conflict, please resolve conflicts." 19 | exit 1; 20 | else 21 | echo "$(tput setaf 2)No conflicts, continuing with commit.$(tput sgr0)" 22 | fi 23 | 24 | echo "Checking for replace directives in go.mod" 25 | if [[ -f "go.mod" ]]; then 26 | if [[ $(grep -c "replace" go.mod) -gt 0 ]]; then 27 | replacedirective=1 28 | fi 29 | fi 30 | 31 | if [ $replacedirective != 0 ]; then 32 | echo "$(tput setaf 1)Replace directives found in go.mod. Please do not commit local development redirects.$(tput sgr0)" 33 | exit 1; 34 | else 35 | echo "$(tput setaf 2)No replace directives in go.mod, OK to commit.$(tput sgr0)" 36 | fi 37 | 38 | echo "Checking for root=true in .editorconfig" 39 | 40 | if [[ $(grep -c "root" .editorconfig) -gt 0 ]]; then 41 | echo "$(tput setaf 1)Please do not set root=true in .editorconfig, this prevents global editorconfigs from providing user prefs.$(tput sgr0)" 42 | exit 1; 43 | else 44 | echo "$(tput setaf 2)No root=true in editorconfig. OK to commit.$(tput sgr0)" 45 | fi 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | lint: 5 | name: Lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | - uses: actions/setup-go@v1 10 | with: 11 | go-version: 1.12 12 | 13 | - name: Install dependencies 14 | run: | 15 | go get honnef.co/go/tools/cmd/staticcheck 16 | go get golang.org/x/lint/golint 17 | 18 | - name: Lint 19 | run: /home/runner/go/bin/golint -set_exit_status ./... 20 | 21 | - name: Static check 22 | run: /home/runner/go/bin/staticcheck ./... 23 | 24 | test: 25 | name: Test 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v1 29 | - uses: teserakt-io/gh-actions/go-test@master 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cover.out 2 | test/data 3 | bin/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: golang:1.12 2 | 3 | stages: 4 | - build 5 | - test 6 | 7 | before_script: 8 | # https://docs.gitlab.com/ee/ci/ssh_keys/ 9 | - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' 10 | - eval $(ssh-agent -s) 11 | - echo "$SSH_CI_KEY" | tr -d '\r' | ssh-add - > /dev/null 12 | - mkdir -p ~/.ssh && chmod 600 ~/.ssh 13 | - echo "gitlab.com,35.231.145.151 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=" >> ~/.ssh/known_hosts 14 | 15 | # Force use of ssh urls instead of https for gitlab.com. Has to be done before any go get... 16 | - git config --global url."git@gitlab.com:".insteadOf "https://gitlab.com/" 17 | 18 | - go get golang.org/x/tools/cmd/goimports 19 | - go install golang.org/x/tools/cmd/goimports 20 | 21 | build: 22 | stage: build 23 | script: 24 | - go build 25 | test: 26 | stage: test 27 | script: 28 | - ./scripts/unittest.sh 29 | 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | 5 | builds: 6 | - id: e4client 7 | main: ./cmd/e4client/e4client.go 8 | binary: e4client 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - darwin 13 | - linux 14 | - windows 15 | goarch: 16 | - amd64 17 | - id: e4keygen 18 | main: ./cmd/e4keygen/e4keygen.go 19 | binary: e4keygen 20 | env: 21 | - CGO_ENABLED=0 22 | goos: 23 | - darwin 24 | - linux 25 | - windows 26 | goarch: 27 | - amd64 28 | 29 | archives: 30 | - id: e4client 31 | builds: 32 | - e4client 33 | replacements: 34 | darwin: Darwin 35 | linux: Linux 36 | windows: Windows 37 | 386: i386 38 | amd64: x86_64 39 | files: 40 | - LICENSE 41 | name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 42 | format: tar.gz 43 | format_overrides: 44 | - goos: windows 45 | format: zip 46 | 47 | - id: e4keygen 48 | builds: 49 | - e4keygen 50 | replacements: 51 | darwin: Darwin 52 | linux: Linux 53 | windows: Windows 54 | 386: i386 55 | amd64: x86_64 56 | files: 57 | - LICENSE 58 | name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 59 | format: tar.gz 60 | format_overrides: 61 | - goos: windows 62 | format: zip 63 | 64 | checksum: 65 | name_template: "checksums.txt" 66 | 67 | snapshot: 68 | name_template: "{{ .Tag }}-next" 69 | 70 | changelog: 71 | sort: asc 72 | filters: 73 | exclude: 74 | - "^scripts:" 75 | - "^test:" 76 | 77 | release: 78 | draft: true 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | e4go is maintained by [Teserakt AG](https://teserakt.io) and its team: 4 | 5 | * [@daeMOn63 ](https://github.com/daeMOn63) (Flavien Binet) 6 | * [@diagprov](https://github.com/diagprov) (Antony Vennard) 7 | * [@odeke-em](https://github.com/odeke-em) (Emmanuel Odeke) 8 | * [@veorq](https://github.com/veorq) (JP Aumasson) 9 | 10 | We welcome and encourage third-party contributions to e4go, be it reports of issues encountered while using the software, suggestions of new features, or proposals of patches. 11 | 12 | ## Bug reports 13 | 14 | Bugs, problems, and feature requests should be reported on [GitHub Issues](https://github.com/teserakt-io/e4go/issues). 15 | 16 | If you report a bug, please: 17 | 18 | * Check that it's not already reported in the [GitHub Issues](https://github.com/teserakt-io/e4go/issues). 19 | * Provide information to help us diagnose and ideally reproduce the bug. 20 | 21 | We appreciate feature requests, however we cannot guarantee that all the features requested will be added to e4go. 22 | 23 | ## Patches 24 | 25 | We encourage you to fix a bug or implement a new feature via a [GitHub Pull request](https://github.com/teserakt-io/e4go/pulls), preferably after creating a related issue and referring it in the PR. 26 | 27 | If you contribute code and submit a patch, please note the following: 28 | 29 | * We use Go version >= 1.13 for developing e4go. 30 | * Pull requests should target the `develop` branch. 31 | * Try to follow the established Go [coding conventions](https://golang.org/doc/effective_go.html). 32 | 33 | Also please make sure to create new unit tests covering your code additions. You can execute the tests by running: 34 | 35 | ```bash 36 | ./scripts/unittest.sh 37 | ``` 38 | 39 | or using the go binary; 40 | 41 | ```bash 42 | go test -v ./crypto 43 | ``` 44 | 45 | All third-party contributions will be recognized in the list of contributors. 46 | 47 | ## House rules 48 | 49 | When posting on discussion threads, please be respectful and civil, avoid (passive-)aggressive tone, and try to communicate clearly and succinctly. This is usually better for everyone :-) 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Teserakt AG 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt text](logo.png) 2 | 3 | [![GoDoc][godoc-image]][godoc-url] ![Go](https://github.com/teserakt-io/e4go/workflows/Go/badge.svg?branch=develop) 4 | 5 | - [Introduction](#introduction) 6 | - [Using our client application](#using-our-client-application) 7 | - [Creating a client](#creating-a-client) 8 | * [Symmetric-key client](#symmetric-key-client) 9 | * [Public-key client](#public-key-client) 10 | * [From a saved state](#from-a-saved-state) 11 | * [Client storage](#client-storage) 12 | - [Integration instructions](#integration-instructions) 13 | * [Receiving a message](#receiving-a-message) 14 | * [Transmitting a message](#transmitting-a-message) 15 | * [Handling errors](#handling-errors) 16 | * [Key generation](#key-generation) 17 | * [Bindings](#bindings) 18 | * [Android](#android) 19 | - [Contributing](#contributing) 20 | - [Security](#security) 21 | - [Support](#support) 22 | - [Intellectual property](#intellectual-property) 23 | 24 | ## Introduction 25 | 26 | This repository provides the `e4` Go package, the client library for [Teserakt's E4](https://teserakt.io/e4), and end-to-end encryption and key management framework for MQTT and other publish-subscribe protocols. 27 | 28 | The `e4` package defines a `Client` object that has a minimal interface, making its integration straightforward via the following methods: 29 | * `ProtectMessage(payload []byte, topic string)` takes a cleartext payload to protect and the associated topic, and returns a `[]byte` that is the payload encrypted and authenticated with the topic's key. 30 | 31 | * `Unprotect(protected []byte, topic string)` takes a protected payload and attempts to decrypt and verify it. If `topic` is the special topic reserved for control messages, then the control message is processed and the client's state updated accordingly. 32 | 33 | Note that we talk of message *protection* instead of just *encryption* because the protection operation includes also authentication and replay defense. The *unprotection* operation thus involves decryption and additional checks, and includes the processing of control messages sent by the server. 34 | 35 | E4's server (C2) is necessary to send control messages and manage a fleet of clients through GUIs, APIs, and automation components. 36 | The server can for example deploy key rotation policies, grant and revoke rights, and enable forward secrecy. 37 | 38 | Please [contact us](mailto:contact@teserakt.io) to request access to a private instance of the server, or test the limited public version. 39 | Without the C2 server, the E4 client library can be used to protect messages using static keys, manually managed. 40 | 41 | ## Using our client application 42 | 43 | To try E4 without writing your own application, we created a simple [interactive client application](./cmd/e4client/) that you can use in combination with our [public demo server interface](https://console.demo.teserakt.io). You can directly [download](https://github.com/teserakt-io/e4go/releases) the client's binary for your platform or build it yourself, and then follow the instructions in the client's [README](./cmd/e4client/README.md). 44 | 45 | ## Creating a client 46 | 47 | The following instructions assume that your program imports `e4` as follows: 48 | 49 | ```go 50 | import e4 "github.com/teserakt-io/e4go" 51 | ``` 52 | 53 | The E4 protocol supports both symmetric key and public-key mode. 54 | Depending on the mode, different functions should be used to instantiate a client: 55 | 56 | ### Symmetric-key client 57 | 58 | A symmetric-key client can be created from a 16-byte identifier (type `[]byte`), a 32-byte key (type `[]byte`), and an `e4.ReadWriteSeeker` implementation, used to persist the client's state (see [client storage](#client-storage) section for details): 59 | 60 | ```go 61 | client, err := e4.NewClient(&e4.SymIDAndKey{ID: id, Key: key}, store) 62 | ``` 63 | 64 | A symmetric-key client can also be created from a name (`string` of arbitrary length) and a password (`string` of a least 16 characters), as follows: 65 | 66 | ```go 67 | client, err := e4.NewClient(&e4.SymNameAndPassword{Name: name, Password: password}, store) 68 | ``` 69 | 70 | The latter is a wrapper over `NewSymKeyClient()` that creates the ID by hashing `name` with SHA-3-256, and deriving a key using Argon2. 71 | 72 | ### Public-key client 73 | 74 | A public-key client can be created from a 16-byte identifier (type `[]byte`), an Ed25519 private key (type `ed25519.PrivateKey`), an `e4.ReadWriteSeeker` implementation, which will be used to store the client's state (see [client storage](#client-storage) section for details), and a Curve25519 public key (32-byte `[]byte`): 75 | 76 | ```go 77 | client, err := e4.NewClient(&e4.PubIDAndKey{ID:id, Key: key, C2PubKey: c2PubKey}, store) 78 | ``` 79 | 80 | Compared to the symmetric-key mode, and additional argument is `c2PubKey`, the public key of the C2 server that sends control messages. 81 | 82 | A public-key client can also be created from a name (`string` of arbitrary length) and a password (`string` of a least 16 characters), as follows: 83 | 84 | ```go 85 | client, err := e4.NewClient(&e4.PubNameAndPassword{Name:name, Password: password, C2PubKey: c2PubKey}, store) 86 | ``` 87 | 88 | The Ed25519 private key is then created from a seed that is derived from the password using Argon2. 89 | The Ed25519 public key can also be retrieved: 90 | 91 | ```go 92 | config := &e4.PubNameAndPassword{Name:name, Password: password, C2PubKey: c2PubKey} 93 | pubKey, err := config.PubKey() 94 | ``` 95 | 96 | ### From a saved state 97 | 98 | A client instance can be recovered using the `LoadClient()` helper given an `e4.ReadWriteSeeker` implementation:: 99 | 100 | ```go 101 | client, err := e4.LoadClient(store) 102 | ``` 103 | 104 | Note that a client's state is automatically saved to the provided store when the client is created, and every time its state changes, and therefore does not need be manually saved. 105 | 106 | ### Client storage 107 | 108 | E4 client offer a way to persist its internal state, allowing to shut it down and reload without having to retransmit all the keys, by providing an `e4.ReadWriteSeeker` implementation to the client. This interface is compatible with any [io.ReadWriteSeeker](https://godoc.org/io#ReadWriteSeeker), such as the `os.File` type, which should be the most common option. But it also allows custom implementations for platforms where filesystem isn't available, see the `e4.NewInMemoryStore([]byte)` we provide as an example of custom storage implementation. 109 | 110 | ## Integration instructions 111 | 112 | To integrate E4 into your application, the protect/unprotect logic needs be added between the network layer and the application layer when transmitting/receiving a message. 113 | 114 | This section provides further instructions related to error handling and to the special case of control messages received from the C2 server. 115 | 116 | Note that E4 is essentially an application security layer, therefore it processes the payload of a message (such as an MQTT payload), excluding header fields. 117 | References to "messages" below therefore refer to payload data (or application message),as opposed to the network-level message. 118 | 119 | ### Receiving a message 120 | 121 | Assume that you receive messages over MQTT or Kafka, and have topics and payload defined as 122 | 123 | ```go 124 | var topic string 125 | var message []byte 126 | ``` 127 | 128 | Having instantiated a client, you can then unprotect the message as follows: 129 | 130 | ```go 131 | plaintext, err := client.Unprotect(message, topic) 132 | if err != nil { 133 | // your error reporting here 134 | } 135 | ``` 136 | 137 | If you receive no error, `plaintext` may still be `nil`. This happens when E4 138 | has processed a control message, that is, a message sent by the C2 server, for example to provision or delete a topic key. 139 | In this case, you do not need to act on the message, since E4 has already processed it. If you want to detect this case you can test for 140 | 141 | ```go 142 | if len(plainText) == 0 { ... } 143 | ``` 144 | 145 | or alternatively 146 | 147 | ```go 148 | if client.IsReceivingTopic(topic) 149 | ``` 150 | 151 | which indicates a message on E4's control channel. 152 | You should not have to parse E4's messages yourself. 153 | Control messages are thus deliberately not returned to users. 154 | 155 | If `plaintext` is not `nil` and `err` is nil, your application can proceed with the unprotected, plaintext message. 156 | 157 | ### Transmitting a message 158 | 159 | To protect a message to be transmitted, suppose say that you have the topic and payload defined as: 160 | 161 | ```go 162 | var topic string 163 | var message []byte 164 | ``` 165 | 166 | You can then use the `Protect` method from the client instance as follows: 167 | 168 | ```go 169 | protected, err := client.Protect(message, topic) 170 | if err != nil { 171 | // your error reporting here 172 | } 173 | ``` 174 | 175 | ### Handling errors 176 | 177 | All errors should be reported, and the `plaintext` and `protected` values discarded upon an error, *except potentially in one case*: 178 | if you receive an `ErrTopicKeyNotFound` error from `ProtectMessage()` or `Unprotect()`, it is because the client does not have the key for this topic. 179 | Therefore, 180 | 181 | * When transmitting a message, your application can either discard the message to be sent, or choose to transmit it in clear. 182 | 183 | * When receiving a message, your application can either discard the message (for example if all messages are assumed to be encrypted in your network), or forward the message to the application (if you call `Unprotect()` for all messages yet tolerate the receiving of unencrypted messages over certain topics, which thus don't have a topic key). 184 | 185 | In order to have the key associated to a certain topic, you must instruct the C2 to deliver said topic key to the client. 186 | 187 | ### Key generation 188 | 189 | To ease key creation, we provide a [key generation](./cmd/e4keygen) application that you can use to generate symmetric, Ed25519 or Curve25519 keys needed for E4 operations. 190 | You can [download](https://github.com/teserakt-io/e4go/releases) the binary for your platform or build it yourself, and then follow the instructions in the keygen [README](./cmd/e4keygen/README.md). 191 | 192 | Our key generator relies on Go's `crypto/rand` package, which guarantees cryptographically secure randomness across various platforms. 193 | 194 | ### Bindings 195 | 196 | #### Android 197 | 198 | Latest bindings for Android can be downloaded from the [release page](https://github.com/teserakt-io/e4go/releases). 199 | On an environment having an Android SDK and NDK available, an Android AAR package can be generated invoking the following script: 200 | 201 | ```bash 202 | ./scripts/android_bindings.sh 203 | ``` 204 | 205 | This will generate: 206 | 207 | - `dist/bindings/android/e4.aar`: the Android package, containing compiled Java class and native libraries for most common architectures 208 | - `dist/bindings/android/e4-sources.jar`: the Java source files 209 | 210 | After importing the AAR in your project, E4 client can be created and invoked 211 | in a similar way than the Go version, for example using Kotlin: 212 | 213 | ```kotlin 214 | import java.io.RandomAccessFile 215 | 216 | import io.teserakt.e4.E4 217 | import io.teserakt.e4.SymNameAndPassword 218 | import io.teserakt.crypto.Crypto 219 | 220 | val cfg = SymNameAndPassword() 221 | cfg.name = "deviceXYZ" 222 | cfg.password = "secretForDeviceXYZ" 223 | 224 | val store = FileStore(filesDir.absolutePath + "/" + cfg.name + ".json") 225 | val client = E4.newClient(cfg, store) 226 | 227 | // From here, messages can be protected / unprotected : 228 | val topic = "/deviceXYZ/data"; 229 | val protectedMessage = client.protectMessage("Hello".toByteArray(Charsets.UTF_8), topic) 230 | val unprotectedMessage = client.unprotect(protectedMessage, topic) 231 | ``` 232 | 233 | Here We are using a custom file storage implemented as such: 234 | ```java 235 | import java.io.FileNotFoundException; 236 | import java.io.IOException; 237 | import java.io.RandomAccessFile; 238 | 239 | import io.teserakt.e4.Store; 240 | 241 | public class FileStore implements Store { 242 | private static final int SEEK_START = 0; 243 | private static final int SEEK_CURRENT = 1; 244 | private static final int SEEK_END = 2; 245 | 246 | private RandomAccessFile file; 247 | 248 | public FileStore(String filepath) throws FileNotFoundException { 249 | this.file = new RandomAccessFile(filepath, "rw"); 250 | } 251 | 252 | public long read(byte[] buf) throws IOException { 253 | return this.file.read(buf); 254 | } 255 | 256 | public long write(byte[] buf) throws IOException { 257 | this.file.write(buf); 258 | return buf.length; 259 | } 260 | 261 | public long seek(long offset, long whence) throws Exception { 262 | long abs; 263 | switch((int)whence) { 264 | case SEEK_START: 265 | abs = offset; 266 | break; 267 | case SEEK_CURRENT: 268 | abs = this.file.getChannel().position() + offset; 269 | break; 270 | case SEEK_END: 271 | abs = this.file.length() + offset; 272 | break; 273 | default: 274 | throw new Exception("invalid whence"); 275 | } 276 | if (abs < 0) { 277 | throw new Exception("negative position"); 278 | } 279 | 280 | this.file.getChannel().position(abs); 281 | return abs; 282 | } 283 | } 284 | ``` 285 | 286 | ## Contributing 287 | 288 | Before contributing, please read our [CONTRIBUTING](./CONTRIBUTING.md) guide. 289 | 290 | ## Security 291 | 292 | To report a security vulnerability (or potential vulnerability where private discussion is preferred) see [SECURITY](./SECURITY.md). 293 | 294 | ## Support 295 | 296 | To request support, please contact [team@teserakt.io](mailto:team@teserakt.io). 297 | 298 | ## Intellectual property 299 | 300 | e4go is copyright (c) Teserakt AG 2018-2020, and released under Apache 2.0 License (see [LICENCE](./LICENSE)). 301 | 302 | [godoc-image]: https://godoc.org/github.com/teserakt-io/e4go?status.svg 303 | [godoc-url]: https://godoc.org/github.com/teserakt-io/e4go 304 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Reporting a security issue 2 | 3 | The Teserakt team recognizes the important contributions the security research community can make. We therefore encourage reporting security issues with the code contained in this repository. 4 | 5 | If you believe you have discovered a security vulnerability, please send an email to security@teserakt.io. You may send an encrypted message using the public key below. 6 | 7 | ``` 8 | -----BEGIN PGP PUBLIC KEY BLOCK----- 9 | 10 | mQENBF1vgCYBCADBfx941J5/VK2j6vazdC2D6XBkMRo/vAQeSFRnYlrcN2aLbD0w 11 | PIpETatsfJKwmAikDwCj/Zdh90K6YRy/c3/OLQ6Kz+V/PvUK9w17hUGdfmuPgc+f 12 | EqMRvsFnegNEMX/Sn/O8DDwRahezk4rEe0Rx7c6FvN3ViclgH+Z6knE9340eqm5r 13 | gcm9TYWu7i1KsmTp9tZchE7dRykI5TwEQ3QfQTlMeudnVObgEslZlZ6I9RYwB71u 14 | Slrlt/gRciHFuwDZSjOz0qcjcGpoNIwnR7wZtXxvqh1oWRFqA18iABlbbNb3B7Xy 15 | syQIXtVGongiIX7jUrLxpXWgsM6c0m7G2B8fABEBAAG0LVRlc2VyYWt0IFNlY3Vy 16 | aXR5IFRlYW0gPHNlY3VyaXR5QHRlc2VyYWt0LmlvPokBTgQTAQoAOBYhBD/evl+N 17 | fyfOFQ1KFFjWk+JNukulBQJdb4AmAhsDBQsJCAcDBRUKCQgLBRYDAgEAAh4BAheA 18 | AAoJEFjWk+JNukulbJUH/0/4T4qR7XS60E8/jxAf9eRNyxTWkL/MdpvdERlbURnB 19 | cp4OOCAkX5h+qcwJrJpzof9/h0I3lr0fHR8kP/uTKnG/fL8JXuIL215RVAueRjSF 20 | I9EREGNebef/BE+9X8Xcjn82BZJLm/0YoTew76wLpkCq3EIkPpiVY/jQ23nv//5W 21 | KWkFiowEFVr89f7e1EQDCctIhTpnDDQR0HIQMJGZUYak6oml4pKlgnySwUZGV+QQ 22 | oBCHSzD6Hd4ekNoxCgzRdbxyWU70TUaQrCA4Pvf3U5L5ljyyHEvErvG7UDEm6DxL 23 | N4AxlyoES4r/uZfdnC13wBcxfTo8dhOv7uBskJug54q5AQ0EXW+AJgEIAKi+As0k 24 | pGi+LGiabhssp8rcYyUhjF76fk9PRXP7iy5DIuvnIroJdgAFh8SbwBDlYk8RqYi1 25 | XhCo75mDjdKFcDNWaA/f7KdKTLgmckcaERiHhqerp1il8q6pXCDdmmMgcpirZ6nE 26 | jxaJ9tbBK49/YpoapnEz5uJpIgw0kaqxplc5v3H+PJ/NSChvI5U3ZQHJ/vNColdO 27 | tGXWvI/bikDd1XSOO10PQZyOgX589rZBhpBpItRWjb5QwezMtoCv5xQZY4fBKmKA 28 | mdKdJJRO3g8g93QBb2kDgz4g5do7dYOyrk0WRTfDntkskUKxtlMEaobrRIsjYXpq 29 | ikZJCwE6wFlR+qMAEQEAAYkBNgQYAQoAIBYhBD/evl+NfyfOFQ1KFFjWk+JNukul 30 | BQJdb4AmAhsMAAoJEFjWk+JNukul/7YH/RZCjrfZb4WvoE8xS9DruizdGq1i0Q7B 31 | tfFr4mGQ+huIF5yafQRWPV4KIFFpDax0B8yAB8QbpYMvCBopCGOguGnNy7GBLi45 32 | h/mV+YCaWluVntMn8fjJS5J+BQQgieu3QJ1cIjKV/RfoXfw1UmwvU0Qg7fvlKKWm 33 | BNz+lK9Dr7DuSZuk/uv6MyWmCn0F7XYjqw9yCvrb1ppyTagF7fSL6zWA/8uCBQpB 34 | 7EVqYqdoM9wx4zoui5fCfRJl8435EjPRlD2LPnKoQQNyjCXznqyri12ShtjgItBk 35 | /cgVUjYnm5bRa+w5TT9uOyPQ9kDSnIszTg3t26Di0KhESUid4FxiV40= 36 | =LsGO 37 | -----END PGP PUBLIC KEY BLOCK----- 38 | ``` 39 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package e4 provides a e4 client implementation and libraries. 16 | // 17 | // It aims to be quick and easy to integrate in IoT devices applications 18 | // enabling to secure their communications, as well as exposing a way to manage the various keys required. 19 | // 20 | // Protecting and unprotecting messages 21 | // 22 | // Once created, a client provide methods to protect messages before sending them to the broker: 23 | // protectedMessage, err := client.ProtectMessage([]byte("secret message"), topicKey) 24 | // or unprotecting the messages it receives. 25 | // originalMessage, err := client.Unprotect([]byte(protectedMessage, topicKey)) 26 | // 27 | // ReceivingTopic and client commands 28 | // 29 | // A special topic (called ReceivingTopic) is reserved to communicate protected commands to the client. 30 | // Such commands are used to update the client state, like setting a new key for a topic, or renewing its private key. 31 | // There is nothing particular to be done when receiving a command, just passing its protected form to the Unprotect() method 32 | // and the client will automatically unprotect and process it (thus returning no unprotected message). 33 | // See commands.go for the list of available commands and their respective parameters. 34 | package e4 35 | 36 | import ( 37 | "bytes" 38 | "encoding/binary" 39 | "encoding/hex" 40 | "encoding/json" 41 | "errors" 42 | "fmt" 43 | "io" 44 | "log" 45 | "sync" 46 | "time" 47 | 48 | miscreant "github.com/miscreant/miscreant.go" 49 | "golang.org/x/crypto/ed25519" 50 | 51 | e4crypto "github.com/teserakt-io/e4go/crypto" 52 | "github.com/teserakt-io/e4go/keys" 53 | ) 54 | 55 | const ( 56 | idTopicPrefix = "e4/" 57 | ) 58 | 59 | var ( 60 | // ErrTopicKeyNotFound occurs when a topic key is missing when encryption/decrypting 61 | ErrTopicKeyNotFound = errors.New("topic key not found") 62 | // ErrUnsupportedOperation occurs when trying to manipulate client public keys with a ClientKey not supporting it 63 | ErrUnsupportedOperation = errors.New("this operation is not supported") 64 | ) 65 | 66 | // Client defines interface for protecting and unprotecting E4 messages and commands 67 | type Client interface { 68 | // ProtectMessage will encrypt the given payload using the key associated to topic. 69 | // When the client doesn't have a key for this topic, ErrTopicKeyNotFound will be returned. 70 | // When no errors, the protected cipher bytes are returned 71 | ProtectMessage(payload []byte, topic string) ([]byte, error) 72 | // Unprotect attempts to decrypt the given cipher using the topic key. 73 | // When the client doesn't have a key for this topic, ErrTopicKeyNotFound will be returned. 74 | // When no errors, the clear payload bytes are returned, unless the protected message was a client command. 75 | // Message are client commands when received on the client receiving topic. The command will be processed 76 | // when unprotecting it, making a nil,nil response indicating a success 77 | Unprotect(protected []byte, topic string) ([]byte, error) 78 | // IsReceivingTopic returns true when the given topic is the client receiving topics. 79 | // Message received from this topics will be protected commands, meant to update the client state 80 | IsReceivingTopic(topic string) bool 81 | // GetReceivingTopic returns the receiving topic for this client, which will be used to transmit commands 82 | // allowing to update the client state, like setting a new private key or adding a new topic key. 83 | GetReceivingTopic() string 84 | 85 | // setIDKey will set the client's key material private key to the given key 86 | setIDKey(key []byte) error 87 | // setPubKey set the public key for the given clientID, if the client key material support it. 88 | // otherwise, ErrUnsupportedOperation is returned 89 | setPubKey(key ed25519.PublicKey, clientID []byte) error 90 | // removePubKey remove the public key for the given clientID, if the client key material support it. 91 | // otherwise, ErrUnsupportedOperation is returned 92 | removePubKey(clientID []byte) error 93 | // resetPubKeys remove all pubKeys from the key material, if it support it. 94 | // otherwise, ErrUnsupportedOperation is returned 95 | resetPubKeys() error 96 | // getPubKeys returns the map of public keys having been set on the client, if the client key material support it. 97 | // otherwise, ErrUnsupportedOperation is returned 98 | getPubKeys() (map[string]ed25519.PublicKey, error) 99 | // setTopicKey set the key for the given topic hash (see crypto.HashTopic to obtain topic hashes). 100 | // Setting topic keys is required prior being able to communicate over this topic. 101 | setTopicKey(key, topicHash []byte) error 102 | // removeTopic will remove the topic key from the client for the given topic hash (see crypto.HashTopic to obtain topic hashes). 103 | removeTopic(topicHash []byte) error 104 | // resetTopics will remove all previously set topics from the client. 105 | resetTopics() error 106 | // setC2Key instructs the device to replace the current C2 public key with the newly transmitted one. 107 | setC2Key(newC2PubKey e4crypto.Curve25519PublicKey) error 108 | } 109 | 110 | // client implements Client interface 111 | type client struct { 112 | ID []byte 113 | // TopicKeys maps a topic hash to a key 114 | // (slices []byte can't be map keys, converting to strings) 115 | TopicKeys map[string]keys.TopicKey 116 | 117 | Key keys.KeyMaterial 118 | 119 | ReceivingTopic string 120 | 121 | store ReadWriteSeeker 122 | lock sync.RWMutex 123 | } 124 | 125 | var _ Client = (*client)(nil) 126 | 127 | // ClientConfig defines an interface for client configuration 128 | type ClientConfig interface { 129 | genNewClient(store ReadWriteSeeker) (*client, error) 130 | } 131 | 132 | // SymIDAndKey defines a configuration to create an E4 client in symmetric key mode 133 | // from an ID and a symmetric key 134 | type SymIDAndKey struct { 135 | ID []byte 136 | Key []byte 137 | } 138 | 139 | // SymNameAndPassword defines a configuration to create an E4 client in symmetric key mode 140 | // from a name and a password. 141 | // The password must contains at least 16 characters. 142 | type SymNameAndPassword struct { 143 | Name string 144 | Password string 145 | } 146 | 147 | // PubIDAndKey defines a configuration to create an E4 client in public key mode 148 | // from an ID, an ed25519 private key, and a curve25519 public key. 149 | type PubIDAndKey struct { 150 | ID []byte 151 | Key e4crypto.Ed25519PrivateKey 152 | C2PubKey e4crypto.Curve25519PublicKey 153 | } 154 | 155 | // PubNameAndPassword defines a configuration to create an E4 client in public key mode 156 | // from a name, a password and a curve25519 public key. 157 | // The password must contains at least 16 characters. 158 | type PubNameAndPassword struct { 159 | Name string 160 | Password string 161 | C2PubKey e4crypto.Curve25519PublicKey 162 | } 163 | 164 | var _ ClientConfig = (*SymIDAndKey)(nil) 165 | var _ ClientConfig = (*SymNameAndPassword)(nil) 166 | var _ ClientConfig = (*PubIDAndKey)(nil) 167 | var _ ClientConfig = (*PubNameAndPassword)(nil) 168 | 169 | func (ik *SymIDAndKey) genNewClient(store ReadWriteSeeker) (*client, error) { 170 | var newID []byte 171 | if len(ik.ID) == 0 { 172 | newID = e4crypto.RandomID() 173 | } else { 174 | newID = make([]byte, len(ik.ID)) 175 | copy(newID, ik.ID) 176 | } 177 | 178 | symKeyMaterial, err := keys.NewSymKeyMaterial(ik.Key) 179 | if err != nil { 180 | return nil, fmt.Errorf("failed to created symkey from key: %v", err) 181 | } 182 | 183 | return newClient(newID, symKeyMaterial, store) 184 | } 185 | 186 | func (np *SymNameAndPassword) genNewClient(store ReadWriteSeeker) (*client, error) { 187 | id := e4crypto.HashIDAlias(np.Name) 188 | 189 | key, err := e4crypto.DeriveSymKey(np.Password) 190 | if err != nil { 191 | return nil, fmt.Errorf("failed to derive key from password: %v", err) 192 | } 193 | 194 | symKeyMaterial, err := keys.NewSymKeyMaterial(key) 195 | if err != nil { 196 | return nil, fmt.Errorf("failed to created symkey from key: %v", err) 197 | } 198 | 199 | return newClient(id, symKeyMaterial, store) 200 | } 201 | 202 | func (ik *PubIDAndKey) genNewClient(store ReadWriteSeeker) (*client, error) { 203 | var newID []byte 204 | if len(ik.ID) == 0 { 205 | newID = e4crypto.RandomID() 206 | } else { 207 | newID = make([]byte, len(ik.ID)) 208 | copy(newID, ik.ID) 209 | } 210 | 211 | pubKeyMaterialKey, err := keys.NewPubKeyMaterial(newID, ik.Key, ik.C2PubKey) 212 | if err != nil { 213 | return nil, fmt.Errorf("failed to create ed25519key from key: %v", err) 214 | } 215 | 216 | return newClient(newID, pubKeyMaterialKey, store) 217 | } 218 | 219 | func (np *PubNameAndPassword) genNewClient(store ReadWriteSeeker) (*client, error) { 220 | id := e4crypto.HashIDAlias(np.Name) 221 | 222 | key, err := e4crypto.Ed25519PrivateKeyFromPassword(np.Password) 223 | if err != nil { 224 | return nil, fmt.Errorf("failed to create ed25519 key from password: %v", err) 225 | } 226 | 227 | pubKeyMaterialKey, err := keys.NewPubKeyMaterial(id, key, np.C2PubKey) 228 | if err != nil { 229 | return nil, fmt.Errorf("failed to create ed25519key from key: %v", err) 230 | } 231 | 232 | return newClient(id, pubKeyMaterialKey, store) 233 | } 234 | 235 | // PubKey returns the ed25519.PublicKey derived from the password 236 | func (np *PubNameAndPassword) PubKey() (e4crypto.Ed25519PublicKey, error) { 237 | key, err := e4crypto.Ed25519PrivateKeyFromPassword(np.Password) 238 | if err != nil { 239 | return nil, fmt.Errorf("failed to create ed25519 key from password: %v", err) 240 | } 241 | 242 | edKey, ok := ed25519.PrivateKey(key).Public().(ed25519.PublicKey) 243 | if !ok { 244 | return nil, errors.New("failed to cast key to ed25519.PublicKey") 245 | } 246 | 247 | return edKey, nil 248 | } 249 | 250 | // ClientOption is an option for an E4 client 251 | type ClientOption interface { 252 | apply(c *client) 253 | } 254 | 255 | type withReceivingTopic string 256 | 257 | func (w withReceivingTopic) apply(c *client) { 258 | c.ReceivingTopic = string(w) 259 | } 260 | 261 | // WithReceivingTopic returns a ClientOption that override the default receiving topic of the client 262 | func WithReceivingTopic(topic string) ClientOption { 263 | return withReceivingTopic(topic) 264 | } 265 | 266 | // NewClient creates a new E4 client, working either in symmetric key mode, or public key mode 267 | // depending the given ClientConfig 268 | // 269 | // config is a ClientConfig, either SymIDAndKey, SymNameAndPassword, PubIDAndKey or PubNameAndPassword 270 | // store is an e4.ReadWriteSeeker implementation 271 | // opts is an optional list of ClientOption allowing to change E4 client defaults 272 | func NewClient(config ClientConfig, store ReadWriteSeeker, opts ...ClientOption) (Client, error) { 273 | c, err := config.genNewClient(store) 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | for _, opt := range opts { 279 | opt.apply(c) 280 | } 281 | 282 | return c, nil 283 | } 284 | 285 | // newClient creates a new client, generating a random ID if they are empty 286 | func newClient(id []byte, clientKey keys.KeyMaterial, store ReadWriteSeeker) (*client, error) { 287 | if len(id) == 0 { 288 | return nil, errors.New("client id must not be empty") 289 | } 290 | 291 | c := &client{ 292 | Key: clientKey, 293 | TopicKeys: make(map[string]keys.TopicKey), 294 | ReceivingTopic: TopicForID(id), 295 | 296 | store: store, 297 | } 298 | 299 | c.ID = make([]byte, len(id)) 300 | copy(c.ID, id) 301 | 302 | log.SetPrefix("e4client\t") 303 | 304 | if err := c.save(); err != nil { 305 | return nil, err 306 | } 307 | 308 | return c, nil 309 | } 310 | 311 | // LoadClient loads a client state from the file system 312 | func LoadClient(store ReadWriteSeeker) (Client, error) { 313 | c := &client{} 314 | 315 | decoder := json.NewDecoder(store) 316 | err := decoder.Decode(c) 317 | if err != nil { 318 | return nil, err 319 | } 320 | 321 | if _, err := store.Seek(0, io.SeekStart); err != nil { 322 | return nil, err 323 | } 324 | 325 | c.store = store 326 | 327 | return c, nil 328 | } 329 | 330 | func (c *client) save() error { 331 | encoder := json.NewEncoder(c.store) 332 | if err := encoder.Encode(c); err != nil { 333 | log.Printf("failed to save client: %v", err) 334 | return err 335 | } 336 | 337 | if _, err := c.store.Seek(0, io.SeekStart); err != nil { 338 | log.Printf("failed to save client: %v", err) 339 | return err 340 | } 341 | 342 | return nil 343 | } 344 | 345 | func (c *client) UnmarshalJSON(data []byte) error { 346 | m := make(map[string]json.RawMessage) 347 | if err := json.Unmarshal(data, &m); err != nil { 348 | return fmt.Errorf("failed to unmarshal client from json: %v", err) 349 | } 350 | 351 | if rawKey, ok := m["Key"]; ok { 352 | clientKey, err := keys.FromRawJSON(rawKey) 353 | if err != nil { 354 | return fmt.Errorf("failed to unmarshal client key: %v", err) 355 | } 356 | 357 | c.Key = clientKey 358 | } 359 | 360 | if rawReceivingTopic, ok := m["ReceivingTopic"]; ok { 361 | if err := json.Unmarshal(rawReceivingTopic, &c.ReceivingTopic); err != nil { 362 | return fmt.Errorf("failed to unmarshal client receiving topic: %v", err) 363 | } 364 | } 365 | 366 | if rawTopicKeys, ok := m["TopicKeys"]; ok { 367 | if err := json.Unmarshal(rawTopicKeys, &c.TopicKeys); err != nil { 368 | return fmt.Errorf("failed to unmarshal client topicKeys: %v", err) 369 | } 370 | } 371 | 372 | if rawID, ok := m["ID"]; ok { 373 | if err := json.Unmarshal(rawID, &c.ID); err != nil { 374 | return fmt.Errorf("failed to unmarshal client ID: %v", err) 375 | } 376 | } 377 | 378 | return nil 379 | } 380 | 381 | // ProtectMessage will protect given payload, given 382 | // the client holds a key for the given topic, otherwise 383 | // ErrTopicKeyNotFound will be returned 384 | func (c *client) ProtectMessage(payload []byte, topic string) ([]byte, error) { 385 | topicHash := hex.EncodeToString(e4crypto.HashTopic(topic)) 386 | 387 | c.lock.RLock() 388 | topicKey, ok := c.TopicKeys[topicHash] 389 | c.lock.RUnlock() 390 | if !ok { 391 | return nil, ErrTopicKeyNotFound 392 | } 393 | 394 | protected, err := c.Key.ProtectMessage(payload, topicKey) 395 | if err != nil { 396 | return nil, err 397 | } 398 | 399 | return protected, nil 400 | } 401 | 402 | // Unprotect will attempt to unprotect the given payload and return the clear message 403 | // The client holds a key for the given topic, otherwise a ErrTopicKeyNotFound error will be returned 404 | // 405 | // In case the protected message is a command (when the topic is identical to the client control topic), 406 | // Unprotect will also process it, returning errors when it is invalid or missing required 407 | // arguments. On success, Unprotecting a command will return nil, nil 408 | func (c *client) Unprotect(protected []byte, topic string) ([]byte, error) { 409 | if topic == c.ReceivingTopic { 410 | command, err := c.Key.UnprotectCommand(protected) 411 | if err != nil { 412 | return nil, err 413 | } 414 | 415 | err = processCommand(c, command) 416 | if err != nil { 417 | return nil, err 418 | } 419 | 420 | return nil, nil 421 | } 422 | 423 | topicHash := e4crypto.HashTopic(topic) 424 | c.lock.RLock() 425 | key, ok := c.TopicKeys[hex.EncodeToString(topicHash)] 426 | c.lock.RUnlock() 427 | if !ok { 428 | return nil, ErrTopicKeyNotFound 429 | } 430 | 431 | message, err := c.Key.UnprotectMessage(protected, key) 432 | 433 | if err == nil { 434 | return message, nil 435 | } 436 | 437 | if err != miscreant.ErrNotAuthentic { 438 | return nil, err 439 | } 440 | 441 | // Since decryption failed, try the previous key if it exists and not too old. 442 | hashOfHash := hex.EncodeToString(e4crypto.HashTopic(string(topicHash))) 443 | topicKeyTs, ok := c.TopicKeys[hashOfHash] 444 | if !ok { 445 | return nil, miscreant.ErrNotAuthentic 446 | } 447 | if len(topicKeyTs) != e4crypto.KeyLen+e4crypto.TimestampLen { 448 | return nil, errors.New("invalid old topic key length") 449 | } 450 | topicKey := make([]byte, e4crypto.KeyLen) 451 | copy(topicKey, topicKeyTs[:e4crypto.KeyLen]) 452 | timestamp := topicKeyTs[e4crypto.KeyLen:] 453 | if err := e4crypto.ValidateTimestampKey(timestamp); err != nil { 454 | return nil, err 455 | } 456 | 457 | return c.Key.UnprotectMessage(protected, topicKey) 458 | } 459 | 460 | // IsReceivingTopic indicate when the given topic is the receiving topic of the client. 461 | // This means message received on this topic are client commands 462 | func (c *client) IsReceivingTopic(topic string) bool { 463 | return topic == c.ReceivingTopic 464 | } 465 | 466 | // GetReceivingTopic returns the client receiving topic. 467 | func (c *client) GetReceivingTopic() string { 468 | return c.ReceivingTopic 469 | } 470 | 471 | // setTopicKey adds a key to the given topic hash, erasing any previous entry 472 | func (c *client) setTopicKey(key, topicHash []byte) error { 473 | if err := e4crypto.ValidateTopicHash(topicHash); err != nil { 474 | return fmt.Errorf("invalid topic hash: %v", err) 475 | } 476 | 477 | c.lock.Lock() 478 | defer c.lock.Unlock() 479 | 480 | topicHashHex := hex.EncodeToString(topicHash) 481 | 482 | // Key transition, if a key already exists for this topic 483 | topicKey, ok := c.TopicKeys[topicHashHex] 484 | if ok { 485 | // Only do key transition if the key received is distinct from the current one 486 | if !bytes.Equal(topicKey, key) { 487 | hashOfHash := e4crypto.HashTopic(string(topicHash)) 488 | timestamp := make([]byte, e4crypto.TimestampLen) 489 | binary.LittleEndian.PutUint64(timestamp, uint64(time.Now().Unix())) 490 | topicKey = append(topicKey, timestamp...) 491 | c.TopicKeys[hex.EncodeToString(hashOfHash)] = topicKey 492 | } 493 | } 494 | 495 | newKey := make([]byte, e4crypto.KeyLen) 496 | copy(newKey, key) 497 | c.TopicKeys[topicHashHex] = newKey 498 | return c.save() 499 | } 500 | 501 | // removeTopic removes the key of the given topic hash 502 | func (c *client) removeTopic(topicHash []byte) error { 503 | if err := e4crypto.ValidateTopicHash(topicHash); err != nil { 504 | return fmt.Errorf("invalid topic hash: %v", err) 505 | } 506 | 507 | c.lock.Lock() 508 | defer c.lock.Unlock() 509 | 510 | delete(c.TopicKeys, hex.EncodeToString(topicHash)) 511 | 512 | // Delete key kept for key transition, if any 513 | hashOfHash := e4crypto.HashTopic(string(topicHash)) 514 | delete(c.TopicKeys, hex.EncodeToString(hashOfHash)) 515 | 516 | return c.save() 517 | } 518 | 519 | // resetTopics removes all topic keys 520 | func (c *client) resetTopics() error { 521 | c.lock.Lock() 522 | defer c.lock.Unlock() 523 | 524 | c.TopicKeys = make(map[string]keys.TopicKey) 525 | return c.save() 526 | } 527 | 528 | // getPubKeys return the list of public keys stored on the client 529 | func (c *client) getPubKeys() (map[string]ed25519.PublicKey, error) { 530 | c.lock.RLock() 531 | defer c.lock.RUnlock() 532 | 533 | pkStore, ok := c.Key.(keys.PubKeyStore) 534 | if !ok { 535 | return nil, ErrUnsupportedOperation 536 | } 537 | 538 | return pkStore.GetPubKeys(), nil 539 | } 540 | 541 | // setPubKey adds a key to the given clientID, erasing any previous entry 542 | func (c *client) setPubKey(key ed25519.PublicKey, clientID []byte) error { 543 | c.lock.Lock() 544 | defer c.lock.Unlock() 545 | 546 | pkStore, ok := c.Key.(keys.PubKeyStore) 547 | if !ok { 548 | return ErrUnsupportedOperation 549 | } 550 | 551 | if err := e4crypto.ValidateID(clientID); err != nil { 552 | return fmt.Errorf("invalid client ID: %v", err) 553 | } 554 | 555 | pkStore.AddPubKey(clientID, key) 556 | 557 | return c.save() 558 | } 559 | 560 | // removePubKey removes the pubkey of the given client id 561 | func (c *client) removePubKey(clientID []byte) error { 562 | c.lock.Lock() 563 | defer c.lock.Unlock() 564 | 565 | pkStore, ok := c.Key.(keys.PubKeyStore) 566 | if !ok { 567 | return ErrUnsupportedOperation 568 | } 569 | 570 | if err := e4crypto.ValidateID(clientID); err != nil { 571 | return fmt.Errorf("invalid client ID: %v", err) 572 | } 573 | 574 | err := pkStore.RemovePubKey(clientID) 575 | if err != nil { 576 | return err 577 | } 578 | 579 | return c.save() 580 | } 581 | 582 | // resetPubKeys removes all public keys 583 | func (c *client) resetPubKeys() error { 584 | c.lock.Lock() 585 | defer c.lock.Unlock() 586 | 587 | pkStore, ok := c.Key.(keys.PubKeyStore) 588 | if !ok { 589 | return ErrUnsupportedOperation 590 | } 591 | 592 | pkStore.ResetPubKeys() 593 | 594 | return c.save() 595 | } 596 | 597 | // setIDKey replaces the current ID key with a new one 598 | func (c *client) setIDKey(key []byte) error { 599 | c.lock.Lock() 600 | defer c.lock.Unlock() 601 | 602 | if err := c.Key.SetKey(key); err != nil { 603 | return err 604 | } 605 | 606 | return c.save() 607 | } 608 | 609 | func (c *client) setC2Key(newC2PubKey e4crypto.Curve25519PublicKey) error { 610 | pkStore, ok := c.Key.(keys.PubKeyStore) 611 | if !ok { 612 | return ErrUnsupportedOperation 613 | } 614 | 615 | if err := e4crypto.ValidateCurve25519PubKey(newC2PubKey); err != nil { 616 | return fmt.Errorf("invalid c2 public key: %v", err) 617 | } 618 | 619 | if err := pkStore.SetC2PubKey(newC2PubKey); err != nil { 620 | return err 621 | } 622 | 623 | return c.save() 624 | } 625 | 626 | // TopicForID generate the receiving topic that a client should subscribe to in order to receive commands 627 | func TopicForID(id []byte) string { 628 | return idTopicPrefix + hex.EncodeToString(id) 629 | } 630 | -------------------------------------------------------------------------------- /cmd/e4client/README.md: -------------------------------------------------------------------------------- 1 | # E4 Go Client 2 | 3 | We provide a simple command-line application to interact with Teserakt's key management server [E4](https://teserakt.io/e4.html). 4 | 5 | You can [download](https://github.com/teserakt-io/e4go/releases) the binary for your platform or build it yourself. 6 | 7 | The program takes the following arguments (note that it does not need to know the server host address, this is the magic of E4): 8 | ``` 9 | Usage of ./bin/e4client: 10 | -broker string 11 | ip:port of the MQTT broker 12 | -c2PubKey string 13 | path to the c2 public key. Required with -pubkey 14 | -name string 15 | The client identifier 16 | -password string 17 | The client password, over 16 characters 18 | -pubkey 19 | Enable public key mode 20 | ``` 21 | 22 | ## Getting started using the demo environment 23 | 24 | We describe how to use the client application in combination with our public demo server instance. 25 | Keep in mind that the server is only for demonstration purposes, and is operated by Teserakt without any guarantee. 26 | Also note that, since the demo platform is public and registration-free, anyone will see your client in the list of devices, and therefore anyone could remove it from the platform, for example. 27 | 28 | ### 1. Create a client instance 29 | 30 | Choose your own deviceID and a password (at least 16 characters), and launch the client: 31 | ``` 32 | ./bin/e4client -name deviceID -password superSecretDevicePassword -broker mqtt.demo.teserakt.io:1883 33 | ``` 34 | This will start an E4 interactive shell, with commands to subscribe to topics and send protected / unprotected messages. 35 | 36 | ``` 37 | E4 Client Interactive Shell 38 | Type help for available commands 39 | --------------------------------- 40 | > help 41 | Available commands: 42 | help show this help 43 | print-key helper to derivate a key from a password and print it as a 32 bytes hex string 44 | e4msg send a protected message on a topic 45 | clearmsg send an unprotected message on a topic 46 | subscribe subscribe to a topic 47 | unsubscribe unsubscribe from a topic 48 | exit exit the application 49 | ``` 50 | 51 | ### 2. Register your client on the demo server 52 | 53 | First, use the `print-key` command to obtain your device key from its password (in our example, the password is "superSecretDevicePassword"): 54 | 55 | ``` 56 | > print-key superSecretDevicePassword 57 | device key: a83d896e7513e929cf63206e9c07629a441d64fb187cae0501f28786ecb8a55d 58 | ``` 59 | 60 | Then head over to `https://console.demo.teserakt.io/clients`, click on "ADD CLIENT", and add your deviceID and the key obtained in the previous step. 61 | 62 | ### 3. Subscribe to a topic and generate a key for it 63 | 64 | In this step, the client will create a new topic, tell the server that messages sent with this topic are to be protected, and tell the server to grant encryption/decryption rights to our device. 65 | 66 | For example, in a real application the topic might be the type of data sent such as telemetry data, the identifier of a subgroup of devices, a secrecy classification level, or the identifier of an ephemeral conversation between two or more devices. 67 | 68 | Using the topic "demoTopic" as an example, first use the client interactive shell to subscribe to the topic, as follows: 69 | 70 | ``` 71 | > subscribe demoTopic 72 | success subscribing to topic 'demoTopic' 73 | ``` 74 | 75 | Then go to `https://console.demo.teserakt.io/topics`, click on "ADD TOPIC", and add your "demoTopic". 76 | This time, no need to generate the key, as the server takes care of it. 77 | 78 | To authorize your client to encrypt and decrypt demoTopic messages, click the "Edit" action pictogram next to the topic created in the list, and add your device. You can also add the topic from the "Edit" menu of the client. 79 | 80 | ### 4. Send and receive messages 81 | 82 | And you're all set, you can now send and read messages on any topic your device is bound to, using the `e4msg` command: 83 | 84 | ``` 85 | > e4msg demoTopic test 86 | received protected message on topic demoTopic: test 87 | protected message has been published on topic demoTopic 88 | ``` 89 | 90 | Using similar operations, you can now create multiple client instances and multiple topics, and use the web UI to manage access rights to protected topics. -------------------------------------------------------------------------------- /cmd/e4client/commands/commands.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package commands 16 | 17 | import ( 18 | "strings" 19 | "time" 20 | 21 | "golang.org/x/crypto/ed25519" 22 | 23 | mqtt "github.com/eclipse/paho.mqtt.golang" 24 | tui "github.com/marcusolsson/tui-go" 25 | 26 | e4 "github.com/teserakt-io/e4go" 27 | e4crypto "github.com/teserakt-io/e4go/crypto" 28 | 29 | "github.com/teserakt-io/e4go/cmd/e4client/logger" 30 | ) 31 | 32 | // Command defines a E4 Client command 33 | type Command struct { 34 | Name string 35 | Help string 36 | ArgsHelp string 37 | Func func([]string) 38 | } 39 | 40 | // SubscribeTopicCommand creates a command to subscribe to MQTT topics 41 | func SubscribeTopicCommand(e4Client e4.Client, mqttClient mqtt.Client, l logger.Logger) *Command { 42 | return &Command{ 43 | Name: "subscribe", 44 | Help: "subscribe to a topic", 45 | ArgsHelp: "", 46 | Func: func(args []string) { 47 | if len(args) != 1 { 48 | l.Print("Usage: subscribe ") 49 | return 50 | } 51 | 52 | token := mqttClient.Subscribe(args[0], 2, func(client mqtt.Client, msg mqtt.Message) { 53 | unprotected, err := e4Client.Unprotect(msg.Payload(), msg.Topic()) 54 | if err != nil { 55 | l.Errorf("failed to unprotect E4 message on topic %s: %v", msg.Topic(), err) 56 | return 57 | } 58 | 59 | l.Printf("< [%s] %s", msg.Topic(), unprotected) 60 | }) 61 | 62 | if err := token.Error(); err != nil { 63 | l.Errorf("got mqtt subscribe error: %v", err) 64 | return 65 | } 66 | 67 | if !token.WaitTimeout(time.Second) { 68 | l.Error("got timeout waiting for mqtt reply") 69 | return 70 | } 71 | 72 | l.Printf("success subscribing to topic '%s'", args[0]) 73 | }, 74 | } 75 | } 76 | 77 | // UnsubscribeTopicCommand creates a command to unsubscribe from MQTT topics 78 | func UnsubscribeTopicCommand(e4Client e4.Client, mqttClient mqtt.Client, l logger.Logger) *Command { 79 | return &Command{ 80 | Name: "unsubscribe", 81 | Help: "unsubscribe from a topic", 82 | ArgsHelp: "", 83 | Func: func(args []string) { 84 | if len(args) != 1 { 85 | l.Print("Usage: unsubscribe ") 86 | return 87 | } 88 | 89 | token := mqttClient.Unsubscribe(args[0]) 90 | 91 | if err := token.Error(); err != nil { 92 | l.Errorf("got mqtt subscribe error: %v", err) 93 | return 94 | } 95 | 96 | if !token.WaitTimeout(time.Second) { 97 | l.Error("got timeout waiting for mqtt reply") 98 | return 99 | } 100 | 101 | l.Printf("success unsubscribing from topic '%s'", args[0]) 102 | }, 103 | } 104 | } 105 | 106 | // SendProtectedMessageCommand creates a command to send a protected message 107 | func SendProtectedMessageCommand(e4Client e4.Client, mqttClient mqtt.Client, l logger.Logger) *Command { 108 | return &Command{ 109 | Name: "e4msg", 110 | Help: "send a protected message on a topic", 111 | ArgsHelp: " ", 112 | Func: func(args []string) { 113 | if len(args) < 2 { 114 | l.Print("Usage: e4msg ") 115 | return 116 | } 117 | 118 | topic := args[0] 119 | message := strings.Join(args[1:], " ") 120 | 121 | payload, err := e4Client.ProtectMessage([]byte(message), topic) 122 | if err != nil { 123 | l.Errorf("failed to protect message: %v", err) 124 | return 125 | } 126 | 127 | token := mqttClient.Publish(topic, 2, true, payload) 128 | if err := token.Error(); err != nil { 129 | l.Errorf("got mqtt publish error: %v", err) 130 | return 131 | } 132 | if !token.WaitTimeout(time.Second) { 133 | l.Errorf("got timeout waiting for mqtt reply") 134 | return 135 | } 136 | 137 | l.Printf("protected message has been published on topic %s", topic) 138 | }, 139 | } 140 | } 141 | 142 | // SendUnprotectedMessageCommand creates a command to send an unprotected message 143 | func SendUnprotectedMessageCommand(e4Client e4.Client, mqttClient mqtt.Client, l logger.Logger) *Command { 144 | return &Command{ 145 | Name: "clearmsg", 146 | Help: "send an unprotected message on a topic", 147 | ArgsHelp: " ", 148 | Func: func(args []string) { 149 | if len(args) < 2 { 150 | l.Print("Usage: clearmsg ") 151 | return 152 | } 153 | 154 | topic := args[0] 155 | message := strings.Join(args[1:], " ") 156 | 157 | token := mqttClient.Publish(topic, 2, true, message) 158 | if err := token.Error(); err != nil { 159 | l.Errorf("got mqtt publish error: %v", err) 160 | return 161 | } 162 | if !token.WaitTimeout(time.Second) { 163 | l.Error("got timeout waiting for mqtt reply") 164 | return 165 | } 166 | 167 | l.Printf("message has been published on topic %s", topic) 168 | }, 169 | } 170 | } 171 | 172 | // PrintKeyCommand print out a hex string key derived from a password 173 | func PrintKeyCommand(l logger.Logger, isPubKeyMode bool) *Command { 174 | return &Command{ 175 | Name: "print-key", 176 | Help: "helper derivating a key from a password and print it as a 32 bytes hex string", 177 | ArgsHelp: "", 178 | Func: func(args []string) { 179 | if len(args) != 1 { 180 | l.Print("Usage: print-key ") 181 | return 182 | } 183 | if isPubKeyMode { 184 | key, err := e4crypto.Ed25519PrivateKeyFromPassword(args[0]) 185 | if err != nil { 186 | l.Printf("failed to generate key from password: %v", err) 187 | return 188 | } 189 | 190 | l.Printf("public key: %x", ed25519.PrivateKey(key).Public()) 191 | } else { 192 | key, err := e4crypto.DeriveSymKey(args[0]) 193 | if err != nil { 194 | l.Printf("failed to generate key: %v", err) 195 | return 196 | } 197 | 198 | l.Printf("key: %x", key) 199 | } 200 | }, 201 | } 202 | } 203 | 204 | // ExitCommand exit the program 205 | func ExitCommand(ui tui.UI) *Command { 206 | return &Command{ 207 | Name: "exit", 208 | Help: "exit the application", 209 | Func: func(args []string) { 210 | ui.Quit() 211 | }, 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /cmd/e4client/e4client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "flag" 21 | "fmt" 22 | "io/ioutil" 23 | "log" 24 | "os" 25 | "regexp" 26 | "strings" 27 | "time" 28 | 29 | mqtt "github.com/eclipse/paho.mqtt.golang" 30 | tui "github.com/marcusolsson/tui-go" 31 | 32 | e4 "github.com/teserakt-io/e4go" 33 | e4crypto "github.com/teserakt-io/e4go/crypto" 34 | 35 | "github.com/teserakt-io/e4go/cmd/e4client/commands" 36 | "github.com/teserakt-io/e4go/cmd/e4client/logger" 37 | ) 38 | 39 | func main() { 40 | var name string 41 | var password string 42 | var pubKeyMode bool 43 | var c2PubKeyPath string 44 | var broker string 45 | 46 | flag.StringVar(&name, "name", "", "The client identifier") 47 | flag.StringVar(&password, "password", "", "The client password, over 16 characters") 48 | flag.BoolVar(&pubKeyMode, "pubkey", false, "Enable public key mode") 49 | flag.StringVar(&c2PubKeyPath, "c2PubKey", "", "path to the c2 curve25519 public key. Required with -pubkey") 50 | flag.StringVar(&broker, "broker", "", "ip:port of the MQTT broker") 51 | flag.Parse() 52 | 53 | log.SetFlags(0) 54 | 55 | if len(name) == 0 { 56 | flag.Usage() 57 | log.Fatal("\n-name is required") 58 | } 59 | 60 | if len(password) < 16 { 61 | flag.Usage() 62 | log.Fatal("\n-password is required and must contains at least 16 characters") 63 | } 64 | 65 | if len(broker) == 0 { 66 | flag.Usage() 67 | log.Fatal("\n-broker is required") 68 | } 69 | 70 | if pubKeyMode && len(c2PubKeyPath) == 0 { 71 | flag.Usage() 72 | log.Fatal("\n-c2pubkey is required") 73 | } 74 | 75 | var c2PubKey []byte 76 | var err error 77 | 78 | if len(c2PubKeyPath) != 0 { 79 | if c2PubKey, err = ioutil.ReadFile(c2PubKeyPath); err != nil { 80 | log.Fatalf("failed to read key from %s: %v\n", c2PubKeyPath, err) 81 | } 82 | } 83 | 84 | history := tui.NewVBox() 85 | historyScroll := tui.NewScrollArea(history) 86 | historyScroll.SetAutoscrollToBottom(true) 87 | 88 | logger := logger.NewTUILogger(history) 89 | 90 | e4Client, err := loadOrCreateClient(name, password, pubKeyMode, c2PubKey) 91 | if err != nil { 92 | log.Fatalf("Failed to load or create E4 client: %v\n", err) 93 | } 94 | logger.Printf("E4 client '%s' initialized\n", name) 95 | 96 | mqttClient, err := initMQTT(broker, name) 97 | if err != nil { 98 | log.Fatalf("Failed to init mqtt client: %v\n", err) 99 | } 100 | logger.Printf("Connected to MQTT broker %s\n", broker) 101 | 102 | if err := subscribeToE4ControlTopic(logger, e4Client, mqttClient); err != nil { 103 | log.Fatalf("Failed to subscribe to e4 client control topic: %v\n", err) 104 | } 105 | logger.Printf("Subscribed to MQTT device control topic %s\n", e4Client.GetReceivingTopic()) 106 | 107 | logger.Print("---------------------------------") 108 | logger.Print("E4 Client Interactive Shell") 109 | logger.Print("Type help for available commands") 110 | logger.Print("---------------------------------") 111 | 112 | historyBox := tui.NewVBox(historyScroll) 113 | historyBox.SetBorder(true) 114 | 115 | input := tui.NewEntry() 116 | input.SetFocused(true) 117 | input.SetSizePolicy(tui.Expanding, tui.Maximum) 118 | 119 | inputBox := tui.NewHBox(input) 120 | inputBox.SetBorder(true) 121 | inputBox.SetSizePolicy(tui.Expanding, tui.Maximum) 122 | 123 | chat := tui.NewVBox(historyBox, inputBox) 124 | chat.SetSizePolicy(tui.Expanding, tui.Expanding) 125 | 126 | ui, err := tui.New(chat) 127 | if err != nil { 128 | log.Fatalf("Failed to init tui: %v\n", err) 129 | } 130 | 131 | commands := []*commands.Command{ 132 | commands.PrintKeyCommand(logger, pubKeyMode), 133 | commands.SendProtectedMessageCommand(e4Client, mqttClient, logger), 134 | commands.SendUnprotectedMessageCommand(e4Client, mqttClient, logger), 135 | commands.SubscribeTopicCommand(e4Client, mqttClient, logger), 136 | commands.UnsubscribeTopicCommand(e4Client, mqttClient, logger), 137 | commands.ExitCommand(ui), 138 | } 139 | 140 | input.OnSubmit(func(e *tui.Entry) { 141 | logger.Printf("> %s", e.Text()) 142 | 143 | args := strings.Split(e.Text(), " ") 144 | if len(args) == 0 { 145 | return 146 | } 147 | 148 | command := args[0] 149 | switch command { 150 | case "help": 151 | pad := len("help") 152 | for _, cmd := range commands { 153 | helpLength := len(cmd.Name) + len(cmd.ArgsHelp) 154 | if helpLength > pad { 155 | pad = helpLength 156 | } 157 | } 158 | 159 | logger.Print("Available commands:") 160 | logger.Printf(" %s %s %s", "help", strings.Repeat(" ", pad-len("help")), "show this help") 161 | for _, cmd := range commands { 162 | logger.Printf(" %s %s %s %s", cmd.Name, cmd.ArgsHelp, strings.Repeat(" ", pad-len(cmd.Name)-len(cmd.ArgsHelp)), cmd.Help) 163 | } 164 | input.SetText("") 165 | default: 166 | for _, cmd := range commands { 167 | if cmd.Name == command { 168 | cmd.Func(args[1:]) 169 | input.SetText("") 170 | return 171 | } 172 | } 173 | 174 | logger.Printf("Unknown command") 175 | input.SetText("") 176 | } 177 | }) 178 | 179 | ui.SetKeybinding("Esc", func() { ui.Quit() }) 180 | ui.SetKeybinding("Tab", func() { 181 | cmdNames := []string{"help"} 182 | for _, cmd := range commands { 183 | cmdNames = append(cmdNames, cmd.Name) 184 | } 185 | 186 | var matches []string 187 | re := regexp.MustCompile(fmt.Sprintf(`^%s`, input.Text())) 188 | for _, name := range cmdNames { 189 | if re.MatchString(name) { 190 | matches = append(matches, name) 191 | } 192 | } 193 | 194 | if len(matches) == 1 { 195 | input.SetText(matches[0]) 196 | } else { 197 | if len(matches) == 0 { 198 | return 199 | } 200 | 201 | logger.Printf("suggestions:\n%s", strings.Join(matches, " ")) 202 | 203 | // Autocomplete as much as we can 204 | if len(matches) > 1 { 205 | shortest := matches[0] 206 | for _, m := range matches { 207 | if len(m) < len(shortest) { 208 | shortest = m 209 | } 210 | } 211 | 212 | for i := len(input.Text()); i < len(shortest); i++ { 213 | for _, m := range matches { 214 | if m[i] != shortest[i] { 215 | if i > 0 { 216 | input.SetText(input.Text() + shortest[len(input.Text()):i]) 217 | } 218 | 219 | return 220 | } 221 | } 222 | } 223 | } 224 | } 225 | }) 226 | 227 | ctx, cancel := context.WithCancel(context.Background()) 228 | defer cancel() 229 | go func() { 230 | for { 231 | select { 232 | case <-time.After(500 * time.Millisecond): 233 | ui.Repaint() 234 | case <-ctx.Done(): 235 | return 236 | } 237 | } 238 | }() 239 | 240 | if err := ui.Run(); err != nil { 241 | log.Fatal(err) 242 | } 243 | } 244 | 245 | func loadOrCreateClient(name, password string, pubKeyMode bool, c2PubKey e4crypto.Curve25519PublicKey) (e4.Client, error) { 246 | var e4Client e4.Client 247 | 248 | savedClientPath := fmt.Sprintf("./%s.json", name) 249 | dstFile, err := os.OpenFile(savedClientPath, os.O_RDWR, 0600) 250 | switch { 251 | case err == nil: 252 | e4Client, err = e4.LoadClient(dstFile) 253 | if err != nil { 254 | return nil, err 255 | } 256 | fmt.Printf("Loaded client from %s\n", savedClientPath) 257 | 258 | default: 259 | if !os.IsNotExist(err) { 260 | fmt.Printf("Failed to load client from file %s: %v\n", savedClientPath, err) 261 | os.Exit(1) 262 | } 263 | 264 | dstFile, err := os.OpenFile(savedClientPath, os.O_CREATE|os.O_RDWR, 0600) 265 | if err != nil { 266 | fmt.Printf("Failed to create client save file %s: %v\n", savedClientPath, err) 267 | os.Exit(1) 268 | } 269 | 270 | var config e4.ClientConfig 271 | if pubKeyMode { 272 | config = &e4.PubNameAndPassword{Name: name, Password: password, C2PubKey: c2PubKey} 273 | } else { 274 | config = &e4.SymNameAndPassword{Name: name, Password: password} 275 | } 276 | 277 | e4Client, err = e4.NewClient(config, dstFile) 278 | if err != nil { 279 | return nil, fmt.Errorf("failed to create E4 client: %v", err) 280 | } 281 | } 282 | 283 | return e4Client, nil 284 | } 285 | 286 | func initMQTT(broker string, name string) (mqtt.Client, error) { 287 | opts := mqtt.NewClientOptions() 288 | opts.AddBroker(broker) 289 | opts.SetClientID(name) 290 | opts.SetCleanSession(true) 291 | 292 | mqttClient := mqtt.NewClient(opts) 293 | 294 | token := mqttClient.Connect() 295 | if err := token.Error(); err != nil { 296 | return nil, err 297 | } 298 | 299 | if !token.WaitTimeout(time.Second) { 300 | return nil, errors.New("got mqtt timeout while connecting to mqtt broker") 301 | } 302 | 303 | return mqttClient, nil 304 | } 305 | 306 | // subscribeToE4ControlTopic will subscribe to the e4 client control topic on MQTT broker, 307 | // allowing it to receives and process commands from the C2 server. 308 | func subscribeToE4ControlTopic(logger logger.Logger, e4Client e4.Client, mqttClient mqtt.Client) error { 309 | token := mqttClient.Subscribe(e4Client.GetReceivingTopic(), 2, func(client mqtt.Client, msg mqtt.Message) { 310 | _, err := e4Client.Unprotect(msg.Payload(), e4Client.GetReceivingTopic()) 311 | if err != nil { 312 | logger.Errorf("E4 unprotect command error: %v", err) 313 | return 314 | } 315 | 316 | logger.Print("success processing E4 command") 317 | }) 318 | 319 | if err := token.Error(); err != nil { 320 | return err 321 | } 322 | 323 | if !token.WaitTimeout(time.Second) { 324 | return errors.New("got mqtt timeout while subscribing to E4 control topic") 325 | } 326 | 327 | return nil 328 | } 329 | -------------------------------------------------------------------------------- /cmd/e4client/logger/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logger 16 | 17 | import ( 18 | "fmt" 19 | 20 | tui "github.com/marcusolsson/tui-go" 21 | ) 22 | 23 | // Logger defines methods to log E4 client messages 24 | type Logger interface { 25 | Errorf(fmtSpec string, args ...interface{}) 26 | Error(message string) 27 | Printf(fmtSpec string, args ...interface{}) 28 | Print(message string) 29 | } 30 | 31 | type tuiLogger struct { 32 | box *tui.Box 33 | } 34 | 35 | // NewTUILogger creates a Logger writing messages in given tui.Box 36 | func NewTUILogger(box *tui.Box) Logger { 37 | return &tuiLogger{ 38 | box: box, 39 | } 40 | } 41 | 42 | // Errorf logs errors in tui.Box, accepting arguments as fmt.Printf does. 43 | func (l *tuiLogger) Errorf(fmtSpec string, args ...interface{}) { 44 | l.Error(fmt.Sprintf(fmtSpec, args...)) 45 | } 46 | 47 | // Error logs errors in tui.Box 48 | func (l *tuiLogger) Error(message string) { 49 | l.Printf("error: %s", message) 50 | } 51 | 52 | // Printf is similar to fmt.Printf, but writing to tui.Box instead of stdout 53 | func (l *tuiLogger) Printf(fmtSpec string, args ...interface{}) { 54 | l.Print(fmt.Sprintf(fmtSpec, args...)) 55 | } 56 | 57 | // Print wrap the given message in tui elements to display in a tui.Box 58 | func (l *tuiLogger) Print(message string) { 59 | l.box.Append(tui.NewHBox( 60 | tui.NewPadder(1, 0, tui.NewLabel(message)), 61 | tui.NewSpacer(), 62 | )) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/e4keygen/README.md: -------------------------------------------------------------------------------- 1 | 2 | # E4 Keygen 3 | 4 | A simple key generator, to help create the various key types used to operate E4 5 | 6 | ## Usage 7 | 8 | ``` 9 | Usage of ./bin/e4keygen: 10 | -force 11 | force overwriting key file if it exists 12 | -out string 13 | folder path where to write the generated key (default: current folder) 14 | -type string 15 | type of the key to generate (one of "symmetric", "ed25519", "curve25519") (default "symmetric") 16 | ``` 17 | 18 | ## Examples 19 | 20 | You can use it to generate client's symmetric key: 21 | ``` 22 | $ ./bin/e4keygen -out ~/e4/keys/client1 -type symmetric 23 | private key successfully written at ~/e4/keys/client1 24 | ``` 25 | 26 | or ed25519 private and public keys: 27 | ``` 28 | $ ./bin/e4keygen -out ~/e4/keys/client1 -type ed25519 29 | private key successfully written at ~/e4/keys/client1 30 | public key successfully written at ~/e4/keys/client1.pub 31 | ``` 32 | 33 | or even curve25519 keys, needed in public key mode: 34 | ``` 35 | $ ./bin/e4keygen -out ~/e4/keys/c2 -type curve25519 36 | private key successfully written at ~/e4/keys/c2 37 | public key successfully written at ~/e4/keys/c2.pub 38 | ``` 39 | -------------------------------------------------------------------------------- /cmd/e4keygen/e4keygen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "log" 21 | "os" 22 | 23 | "golang.org/x/crypto/curve25519" 24 | "golang.org/x/crypto/ed25519" 25 | 26 | e4crypto "github.com/teserakt-io/e4go/crypto" 27 | ) 28 | 29 | // List of supported KeyTypes 30 | const ( 31 | KeyTypeSymmetric = "symmetric" 32 | KeyTypeEd25519 = "ed25519" 33 | KeyTypeCurve25519 = "curve25519" 34 | ) 35 | 36 | func main() { 37 | var keyType string 38 | var out string 39 | var force bool 40 | 41 | log.SetFlags(0) 42 | 43 | keyTypeHelp := fmt.Sprintf("type of the key to generate (one of %q, %q, %q)", KeyTypeSymmetric, KeyTypeEd25519, KeyTypeCurve25519) 44 | 45 | flag.StringVar(&keyType, "type", "symmetric", keyTypeHelp) 46 | flag.StringVar(&out, "out", "", "file path where the key will be generated (required)") 47 | flag.BoolVar(&force, "force", false, "force overwritting key file if it exists") 48 | flag.Parse() 49 | 50 | if len(out) == 0 { 51 | flag.Usage() 52 | os.Exit(1) 53 | } 54 | 55 | var privKey []byte 56 | var pubKey []byte 57 | var err error 58 | 59 | switch keyType { 60 | case KeyTypeSymmetric: 61 | privKey = e4crypto.RandomKey() 62 | case KeyTypeEd25519: 63 | pubKey, privKey, err = ed25519.GenerateKey(nil) 64 | if err != nil { 65 | log.Fatalf("Failed to generate ed25519 key: %v\n", err) 66 | } 67 | case KeyTypeCurve25519: 68 | privKey = e4crypto.RandomKey() 69 | pubKey, err = curve25519.X25519(privKey, curve25519.Basepoint) 70 | if err != nil { 71 | log.Fatalf("Failed to generate curve25519 key: %v\n", err) 72 | } 73 | default: 74 | log.Fatalf("Unknown key type: %s\n", keyType) 75 | } 76 | 77 | if err := writeKey(privKey, pubKey, out, force); err != nil { 78 | log.Fatal(err) 79 | } 80 | } 81 | 82 | func writeKey(privateBytes []byte, publicBytes []byte, filepath string, force bool) error { 83 | if err := write(privateBytes, filepath, 0600, force); err != nil { 84 | return fmt.Errorf("failed to write private key %s: %v", filepath, err) 85 | } 86 | 87 | fmt.Printf("private key successfully written at %s\n", filepath) 88 | 89 | if len(publicBytes) > 0 { 90 | pubKeyFilepath := fmt.Sprintf("%s.pub", filepath) 91 | if err := write(publicBytes, pubKeyFilepath, 0644, force); err != nil { 92 | return fmt.Errorf("failed to write public key %s: %v", pubKeyFilepath, err) 93 | } 94 | fmt.Printf("public key successfully written at %s\n", pubKeyFilepath) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func write(keyBytes []byte, filepath string, perm os.FileMode, force bool) error { 101 | openFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC 102 | if !force { 103 | openFlags = openFlags | os.O_EXCL 104 | } 105 | 106 | keyFile, err := os.OpenFile(filepath, openFlags, perm) 107 | if err != nil { 108 | return err 109 | } 110 | defer keyFile.Close() 111 | 112 | n, err := keyFile.Write(keyBytes) 113 | if err != nil { 114 | return err 115 | } 116 | if g, w := len(keyBytes), n; g != w { 117 | return fmt.Errorf("failed to write public key, got %d bytes, wanted %d", g, w) 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e4 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | 21 | "golang.org/x/crypto/ed25519" 22 | 23 | e4crypto "github.com/teserakt-io/e4go/crypto" 24 | ) 25 | 26 | // List of supported commands 27 | const ( 28 | // RemoveTopic command allows to remove a topic key from the client. 29 | // It expects a topic hash as argument 30 | RemoveTopic byte = iota 31 | // ResetTopics allows to clear out all the topics on a client. 32 | // It doesn't have any argument 33 | ResetTopics 34 | // SetIDKey allows to set the private key of a client. 35 | // It expects a key as argument 36 | SetIDKey 37 | // SetTopicKey allows to add a topic key on the client. 38 | // It takes a key, followed by a topic hash as arguments. 39 | SetTopicKey 40 | // RemovePubKey allows to remove a public key from the client. 41 | // It takes the ID to be removed as argument 42 | RemovePubKey 43 | // ResetPubKeys removes all public keys stored on the client. 44 | // It expects no argument 45 | ResetPubKeys 46 | // SetPubKey allows to set a public key on the client. 47 | // It takes a public key, followed by an ID as arguments. 48 | SetPubKey 49 | // SetC2PubKey replaces the current C2 public key with the newly transmitted one. 50 | SetC2Key 51 | 52 | // UnknownCommand must stay the last element. It's used to 53 | // know if a Command is out of range 54 | UnknownCommand = 0xFF 55 | ) 56 | 57 | var ( 58 | // ErrInvalidCommand is returned when trying to process an unsupported command 59 | ErrInvalidCommand = errors.New("invalid command") 60 | ) 61 | 62 | // processCommand will attempt to parse given command 63 | // and extract arguments to call expected Client method 64 | func processCommand(client Client, payload []byte) error { 65 | cmd, blob := payload[0], payload[1:] 66 | 67 | switch cmd { 68 | case RemoveTopic: 69 | if len(blob) != e4crypto.HashLen { 70 | return errors.New("invalid RemoveTopic length") 71 | } 72 | return client.removeTopic(blob) 73 | 74 | case ResetTopics: 75 | if len(blob) != 0 { 76 | return errors.New("invalid ResetTopics length") 77 | } 78 | return client.resetTopics() 79 | 80 | case SetIDKey: 81 | if len(blob) != e4crypto.KeyLen && len(blob) != ed25519.PrivateKeySize { 82 | return errors.New("invalid SetIDKey length") 83 | } 84 | return client.setIDKey(blob) 85 | 86 | case SetTopicKey: 87 | if len(blob) != e4crypto.KeyLen+e4crypto.HashLen { 88 | return errors.New("invalid SetTopicKey length") 89 | } 90 | return client.setTopicKey(blob[:e4crypto.KeyLen], blob[e4crypto.KeyLen:]) 91 | 92 | case RemovePubKey: 93 | if len(blob) != e4crypto.IDLen { 94 | return errors.New("invalid RemovePubKey length") 95 | } 96 | return client.removePubKey(blob) 97 | 98 | case ResetPubKeys: 99 | if len(blob) != 0 { 100 | return errors.New("invalid ResetPubKeys length") 101 | } 102 | return client.resetPubKeys() 103 | 104 | case SetPubKey: 105 | if len(blob) != ed25519.PublicKeySize+e4crypto.IDLen { 106 | return errors.New("invalid SetPubKey length") 107 | } 108 | return client.setPubKey(blob[:ed25519.PublicKeySize], blob[ed25519.PublicKeySize:]) 109 | case SetC2Key: 110 | if len(blob) != e4crypto.Curve25519PubKeyLen { 111 | return errors.New("invalid SetC2Key length") 112 | } 113 | return client.setC2Key(blob[:e4crypto.Curve25519PubKeyLen]) 114 | 115 | default: 116 | return ErrInvalidCommand 117 | } 118 | } 119 | 120 | // CmdRemoveTopic creates a command to remove the key 121 | // associated with the topic, from the client 122 | func CmdRemoveTopic(topic string) ([]byte, error) { 123 | if len(topic) == 0 { 124 | return nil, errors.New("topic must not be empty") 125 | } 126 | 127 | cmd := append([]byte{RemoveTopic}, e4crypto.HashTopic(topic)...) 128 | 129 | return cmd, nil 130 | } 131 | 132 | // CmdResetTopics creates a command to remove all topic keys stored on the client 133 | func CmdResetTopics() ([]byte, error) { 134 | return []byte{ResetTopics}, nil 135 | } 136 | 137 | // CmdSetIDKey creates a command to set the client private key to the given key 138 | func CmdSetIDKey(key []byte) ([]byte, error) { 139 | keyLen := len(key) 140 | if keyLen != e4crypto.KeyLen && keyLen != ed25519.PrivateKeySize { 141 | return nil, fmt.Errorf("invalid key length, got %d, wanted %d or %d", keyLen, e4crypto.KeyLen, ed25519.PrivateKeySize) 142 | } 143 | 144 | cmd := append([]byte{SetIDKey}, key...) 145 | 146 | return cmd, nil 147 | } 148 | 149 | // CmdSetTopicKey creates a command to set the given 150 | // topic key and its corresponding topic, on the client 151 | func CmdSetTopicKey(topicKey []byte, topic string) ([]byte, error) { 152 | if g, w := len(topicKey), e4crypto.KeyLen; g != w { 153 | return nil, fmt.Errorf("invalid key length, got %d, wanted %d", g, w) 154 | } 155 | 156 | if len(topic) == 0 { 157 | return nil, errors.New("topic must not be empty") 158 | } 159 | 160 | cmd := append([]byte{SetTopicKey}, topicKey...) 161 | cmd = append(cmd, e4crypto.HashTopic(topic)...) 162 | 163 | return cmd, nil 164 | } 165 | 166 | // CmdRemovePubKey creates a command to remove the public key identified by given name from the client 167 | func CmdRemovePubKey(name string) ([]byte, error) { 168 | if len(name) == 0 { 169 | return nil, errors.New("name must not be empty") 170 | } 171 | 172 | cmd := append([]byte{RemovePubKey}, e4crypto.HashIDAlias(name)...) 173 | 174 | return cmd, nil 175 | } 176 | 177 | // CmdResetPubKeys creates a command to removes all public keys from the client 178 | func CmdResetPubKeys() ([]byte, error) { 179 | return []byte{ResetPubKeys}, nil 180 | } 181 | 182 | // CmdSetPubKey creates a command to set a given public key, 183 | // identified by given name on the client 184 | func CmdSetPubKey(pubKey e4crypto.Ed25519PublicKey, name string) ([]byte, error) { 185 | if g, w := len(pubKey), ed25519.PublicKeySize; g != w { 186 | return nil, fmt.Errorf("invalid public key length, got %d, wanted %d", g, w) 187 | } 188 | 189 | if len(name) == 0 { 190 | return nil, errors.New("name must not be empty") 191 | } 192 | 193 | cmd := append([]byte{SetPubKey}, pubKey...) 194 | cmd = append(cmd, e4crypto.HashIDAlias(name)...) 195 | 196 | return cmd, nil 197 | } 198 | 199 | // CmdSetC2Key creates a command to replace the c2 public key by the given one. 200 | func CmdSetC2Key(c2PubKey e4crypto.Curve25519PublicKey) ([]byte, error) { 201 | if err := e4crypto.ValidateCurve25519PubKey(c2PubKey); err != nil { 202 | return nil, fmt.Errorf("invalid c2 public key: %v", err) 203 | } 204 | 205 | cmd := append([]byte{SetC2Key}, c2PubKey...) 206 | 207 | return cmd, nil 208 | } 209 | -------------------------------------------------------------------------------- /commands_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e4 16 | 17 | import ( 18 | "bytes" 19 | "crypto/rand" 20 | "testing" 21 | 22 | "golang.org/x/crypto/curve25519" 23 | "golang.org/x/crypto/ed25519" 24 | 25 | e4crypto "github.com/teserakt-io/e4go/crypto" 26 | ) 27 | 28 | var invalidKeys = [][]byte{ 29 | nil, 30 | []byte{}, 31 | make([]byte, e4crypto.KeyLen-1), 32 | make([]byte, e4crypto.KeyLen+1), 33 | } 34 | 35 | var invalidNames = []string{ 36 | "", 37 | } 38 | 39 | var invalidPubKeys = []ed25519.PublicKey{ 40 | nil, 41 | []byte{}, 42 | make([]byte, ed25519.PublicKeySize-1), 43 | make([]byte, ed25519.PublicKeySize+1), 44 | } 45 | 46 | var invalidCurve25519PubKeys = []e4crypto.Curve25519PublicKey{ 47 | nil, 48 | []byte{}, 49 | make([]byte, e4crypto.Curve25519PubKeyLen-1), 50 | make([]byte, e4crypto.Curve25519PubKeyLen+1), 51 | } 52 | 53 | func TestCmdRemoveTopic(t *testing.T) { 54 | t.Run("invalid names produce errors", func(t *testing.T) { 55 | for _, name := range invalidNames { 56 | _, err := CmdRemoveTopic(name) 57 | if err == nil { 58 | t.Fatalf("got no error with name: %s", name) 59 | } 60 | } 61 | }) 62 | 63 | t.Run("expected command is created", func(t *testing.T) { 64 | topic := "some-topic" 65 | 66 | cmd, err := CmdRemoveTopic(topic) 67 | if err != nil { 68 | t.Fatalf("failed to create command: %v", err) 69 | } 70 | 71 | if got, want := len(cmd), 1+e4crypto.HashLen; got != want { 72 | t.Fatalf("invalid command length, got %d, wanted %d", got, want) 73 | } 74 | 75 | expectedCmd := append([]byte{RemoveTopic}, e4crypto.HashTopic(topic)...) 76 | if !bytes.Equal(cmd, expectedCmd) { 77 | t.Fatalf("invalid command, got %v, wanted %v", cmd, expectedCmd) 78 | } 79 | }) 80 | } 81 | 82 | func TestCmdResetTopics(t *testing.T) { 83 | t.Run("expected command is created", func(t *testing.T) { 84 | cmd, err := CmdResetTopics() 85 | if err != nil { 86 | t.Fatalf("failed to create command: %v", err) 87 | } 88 | 89 | if got, want := len(cmd), 1; got != want { 90 | t.Fatalf("invalid command length, got %d, wanted %d", got, want) 91 | } 92 | 93 | expectedCmd := []byte{ResetTopics} 94 | if !bytes.Equal(cmd, expectedCmd) { 95 | t.Fatalf("invalid command, got %v, wanted %v", cmd, expectedCmd) 96 | } 97 | }) 98 | } 99 | 100 | func TestCmdSetIDKey(t *testing.T) { 101 | t.Run("invalid keys return errors", func(t *testing.T) { 102 | for _, k := range invalidKeys { 103 | _, err := CmdSetIDKey(k) 104 | if err == nil { 105 | t.Fatalf("got no error with key %v", k) 106 | } 107 | } 108 | }) 109 | 110 | t.Run("expected command is created", func(t *testing.T) { 111 | expectedKey := e4crypto.RandomKey() 112 | cmd, err := CmdSetIDKey(expectedKey) 113 | if err != nil { 114 | t.Fatalf("failed to create command: %v", err) 115 | } 116 | 117 | if got, want := len(cmd), 1+e4crypto.KeyLen; got != want { 118 | t.Fatalf("invalid command length, got %d, wanted %d", got, want) 119 | } 120 | 121 | expectedCmd := append([]byte{SetIDKey}, expectedKey...) 122 | if !bytes.Equal(cmd, expectedCmd) { 123 | t.Fatalf("invalid command, got %v, wanted %v", cmd, expectedCmd) 124 | } 125 | }) 126 | } 127 | 128 | func TestCmdSetTopicKey(t *testing.T) { 129 | t.Run("invalid keys produce errors", func(t *testing.T) { 130 | for _, k := range invalidKeys { 131 | _, err := CmdSetTopicKey(k, "some-topic") 132 | if err == nil { 133 | t.Fatalf("got no error with key %v", k) 134 | } 135 | } 136 | }) 137 | 138 | t.Run("invalid names produce errors", func(t *testing.T) { 139 | validKey := e4crypto.RandomKey() 140 | for _, name := range invalidNames { 141 | _, err := CmdSetTopicKey(validKey, name) 142 | if err == nil { 143 | t.Fatalf("got no error with name: %s", name) 144 | } 145 | } 146 | }) 147 | 148 | t.Run("expected command is created", func(t *testing.T) { 149 | expectedKey := e4crypto.RandomKey() 150 | expectedTopic := "some-topic" 151 | cmd, err := CmdSetTopicKey(expectedKey, expectedTopic) 152 | if err != nil { 153 | t.Fatalf("failed to create command: %v", err) 154 | } 155 | 156 | if got, want := len(cmd), 1+e4crypto.KeyLen+e4crypto.HashLen; got != want { 157 | t.Fatalf("invalid command length, got %d, wanted %d", got, want) 158 | } 159 | 160 | expectedCmd := append([]byte{SetTopicKey}, expectedKey...) 161 | expectedCmd = append(expectedCmd, e4crypto.HashTopic(expectedTopic)...) 162 | if !bytes.Equal(cmd, expectedCmd) { 163 | t.Fatalf("invalid command, got %v, wanted %v", cmd, expectedCmd) 164 | } 165 | }) 166 | } 167 | 168 | func TestCmdRemovePubKey(t *testing.T) { 169 | t.Run("invalid names produce errors", func(t *testing.T) { 170 | for _, name := range invalidNames { 171 | _, err := CmdRemovePubKey(name) 172 | if err == nil { 173 | t.Fatalf("got no error with name: %s", name) 174 | } 175 | } 176 | }) 177 | 178 | t.Run("expected command is created", func(t *testing.T) { 179 | expectedName := "some-name" 180 | cmd, err := CmdRemovePubKey(expectedName) 181 | if err != nil { 182 | t.Fatalf("failed to create command: %v", err) 183 | } 184 | 185 | if got, want := len(cmd), 1+e4crypto.IDLen; got != want { 186 | t.Fatalf("invalid command length, got %d, wanted %d", got, want) 187 | } 188 | 189 | expectedCmd := append([]byte{RemovePubKey}, e4crypto.HashIDAlias(expectedName)...) 190 | if !bytes.Equal(cmd, expectedCmd) { 191 | t.Fatalf("invalid command, got %v, wanted %v", cmd, expectedCmd) 192 | } 193 | }) 194 | } 195 | 196 | func TestCmdResetPubKeys(t *testing.T) { 197 | t.Run("expected command is created", func(t *testing.T) { 198 | cmd, err := CmdResetPubKeys() 199 | if err != nil { 200 | t.Fatalf("failed to create command: %v", err) 201 | } 202 | 203 | if got, want := len(cmd), 1; got != want { 204 | t.Fatalf("invalid command length, got %d, wanted %d", got, want) 205 | } 206 | 207 | expectedCmd := []byte{ResetPubKeys} 208 | if !bytes.Equal(cmd, expectedCmd) { 209 | t.Fatalf("invalid command, got %v, wanted %v", cmd, expectedCmd) 210 | } 211 | }) 212 | } 213 | 214 | func TestCmdSetPubKey(t *testing.T) { 215 | t.Run("invalid keys produce errors", func(t *testing.T) { 216 | for _, k := range invalidPubKeys { 217 | _, err := CmdSetPubKey(k, "some-name") 218 | if err == nil { 219 | t.Fatalf("got no error with key %v", k) 220 | } 221 | } 222 | }) 223 | 224 | t.Run("invalid names produce errors", func(t *testing.T) { 225 | validKey, _, err := ed25519.GenerateKey((rand.Reader)) 226 | if err != nil { 227 | t.Fatalf("failed to generate public key: %v", err) 228 | } 229 | 230 | for _, name := range invalidNames { 231 | _, err := CmdSetPubKey(validKey, name) 232 | if err == nil { 233 | t.Fatalf("got no error with name: %s", name) 234 | } 235 | } 236 | }) 237 | 238 | t.Run("expected command is created", func(t *testing.T) { 239 | expectedKey, _, err := ed25519.GenerateKey((rand.Reader)) 240 | if err != nil { 241 | t.Fatalf("failed to generate public key: %v", err) 242 | } 243 | 244 | expectedName := "some-name" 245 | cmd, err := CmdSetPubKey(expectedKey, expectedName) 246 | if err != nil { 247 | t.Fatalf("failed to create command: %v", err) 248 | } 249 | 250 | if got, want := len(cmd), 1+ed25519.PublicKeySize+e4crypto.IDLen; got != want { 251 | t.Fatalf("invalid command length, got %d, wanted %d", got, want) 252 | } 253 | 254 | expectedCmd := append([]byte{SetPubKey}, expectedKey...) 255 | expectedCmd = append(expectedCmd, e4crypto.HashIDAlias(expectedName)...) 256 | if !bytes.Equal(cmd, expectedCmd) { 257 | t.Fatalf("invalid command, got %v, wanted %v", cmd, expectedCmd) 258 | } 259 | }) 260 | } 261 | 262 | func TestCmdSetC2Key(t *testing.T) { 263 | t.Run("invalid keys produce errors", func(t *testing.T) { 264 | for _, k := range invalidCurve25519PubKeys { 265 | _, err := CmdSetC2Key(k) 266 | if err == nil { 267 | t.Fatalf("got no error with key %v", k) 268 | } 269 | } 270 | }) 271 | 272 | t.Run("expected command is created", func(t *testing.T) { 273 | privKey := e4crypto.RandomKey() 274 | expectedKey, err := curve25519.X25519(privKey, curve25519.Basepoint) 275 | if err != nil { 276 | t.Fatalf("failed to generate public key: %v", err) 277 | } 278 | 279 | cmd, err := CmdSetC2Key(expectedKey) 280 | if err != nil { 281 | t.Fatalf("failed to create command: %v", err) 282 | } 283 | 284 | if got, want := len(cmd), 1+e4crypto.Curve25519PubKeyLen; got != want { 285 | t.Fatalf("invalid command length, got %d, wanted %d", got, want) 286 | } 287 | 288 | expectedCmd := append([]byte{SetC2Key}, expectedKey...) 289 | if !bytes.Equal(cmd, expectedCmd) { 290 | t.Fatalf("invalid command, got %v, wanted %v", cmd, expectedCmd) 291 | } 292 | }) 293 | } 294 | -------------------------------------------------------------------------------- /crypto/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package crypto 16 | 17 | import "time" 18 | 19 | // List of global e4 constants 20 | const ( 21 | // IDLen is the length of an E4 ID 22 | IDLen = 16 23 | // KeyLen is the length of a symmetric key 24 | KeyLen = 32 25 | // TagLen is the length of the authentication tag appended to the cipher 26 | TagLen = 16 27 | // HashLen is the length of a hashed topic 28 | HashLen = 16 29 | // TimestampLen is the length of the timestamp 30 | TimestampLen = 8 31 | // MaxTopicLen is the maximum length of a topic 32 | MaxTopicLen = 512 33 | // MaxDelayDuration is the validity time of a protected message 34 | MaxDelayDuration = 10 * time.Minute 35 | // MaxDelayKeyTransition is the validity time of an old topic key once updated 36 | MaxDelayKeyTransition = 60 * time.Minute 37 | // IDLenHex is the length of a hexadecimal encoded ID 38 | IDLenHex = IDLen * 2 39 | // KeyLenHex is the length of a hexadecimal encoded key 40 | KeyLenHex = KeyLen * 2 41 | 42 | // Curve25519PubKeyLen is the length of a curve25519 public key 43 | Curve25519PubKeyLen = 32 44 | // Curve25519PrivKeyLen is the length of a curve25519 private key 45 | Curve25519PrivKeyLen = 32 46 | ) 47 | -------------------------------------------------------------------------------- /crypto/crypto.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package crypto defines the cryptographic functions used in E4 16 | package crypto 17 | 18 | import ( 19 | "crypto/rand" 20 | "encoding/binary" 21 | "errors" 22 | "fmt" 23 | "time" 24 | 25 | miscreant "github.com/miscreant/miscreant.go" 26 | "github.com/teserakt-io/golang-ed25519/extra25519" 27 | "golang.org/x/crypto/argon2" 28 | "golang.org/x/crypto/ed25519" 29 | ) 30 | 31 | var ( 32 | // ErrInvalidProtectedLen occurs when the protected message is not of the expected length 33 | ErrInvalidProtectedLen = errors.New("invalid length of protected message") 34 | // ErrTooShortCiphertext occurs when trying to unprotect a ciphertext shorter than TimestampLen 35 | ErrTooShortCiphertext = errors.New("ciphertext too short") 36 | // ErrTimestampInFuture occurs when the cipher timestamp is in the future 37 | ErrTimestampInFuture = errors.New("timestamp received is in the future") 38 | // ErrTimestampTooOld occurs when the cipher timestamp is older than MaxDelayDuration from now 39 | ErrTimestampTooOld = errors.New("timestamp too old") 40 | // ErrInvalidSignature occurs when a signature verification fails 41 | ErrInvalidSignature = errors.New("invalid signature") 42 | // ErrInvalidSignerID occurs when trying to sign with an invalid ID 43 | ErrInvalidSignerID = errors.New("invalid signer ID") 44 | // ErrInvalidTimestamp occurs when trying to sign with an invalid timestamp 45 | ErrInvalidTimestamp = errors.New("invalid timestamp") 46 | ) 47 | 48 | // Ed25519PublicKey defines an alias for Ed25519 public keys 49 | type Ed25519PublicKey = []byte 50 | 51 | // Ed25519PrivateKey defines an alias for Ed25519 private keys 52 | type Ed25519PrivateKey = []byte 53 | 54 | // Curve25519PublicKey defines an alias for curve 25519 public keys 55 | type Curve25519PublicKey = []byte 56 | 57 | // Curve25519PrivateKey defines an alias for curve 25519 private keys 58 | type Curve25519PrivateKey = []byte 59 | 60 | // Encrypt creates an authenticated ciphertext 61 | func Encrypt(key, ad, pt []byte) ([]byte, error) { 62 | if err := ValidateSymKey(key); err != nil { 63 | return nil, err 64 | } 65 | 66 | // Use same key for CMAC and CTR, negligible security bound difference 67 | doublekey := append(key, key...) 68 | 69 | c, err := miscreant.NewAESCMACSIV(doublekey) 70 | if err != nil { 71 | return nil, err 72 | } 73 | ads := make([][]byte, 1) 74 | ads[0] = ad 75 | return c.Seal(nil, pt, ads...) 76 | } 77 | 78 | // Decrypt decrypts and verifies an authenticated ciphertext 79 | func Decrypt(key, ad, ct []byte) ([]byte, error) { 80 | if err := ValidateSymKey(key); err != nil { 81 | return nil, err 82 | } 83 | 84 | // Use same key for CMAC and CTR, negligible security bound difference 85 | doublekey := append(key, key...) 86 | 87 | c, err := miscreant.NewAESCMACSIV(doublekey) 88 | if err != nil { 89 | return nil, err 90 | } 91 | if len(ct) < c.Overhead() { 92 | return nil, errors.New("too short ciphertext") 93 | } 94 | 95 | return c.Open(nil, ct, ad) 96 | } 97 | 98 | // Sign will sign the given payload using the given privateKey, 99 | // producing an output composed of: timestamp + signedID + payload + signature 100 | func Sign(signerID []byte, privateKey Ed25519PrivateKey, timestamp []byte, payload []byte) ([]byte, error) { 101 | if len(signerID) != IDLen { 102 | return nil, ErrInvalidSignerID 103 | } 104 | 105 | if len(timestamp) != TimestampLen { 106 | return nil, ErrInvalidTimestamp 107 | } 108 | 109 | protected := append(timestamp, signerID...) 110 | protected = append(protected, payload...) 111 | 112 | // sig should always be ed25519.SignatureSize=64 bytes 113 | sig := ed25519.Sign(privateKey, protected) 114 | if len(sig) != ed25519.SignatureSize { 115 | return nil, ErrInvalidSignature 116 | } 117 | protected = append(protected, sig...) 118 | 119 | return protected, nil 120 | } 121 | 122 | // DeriveSymKey derives a symmetric key from a password using Argon2 123 | // (Replaces HashPwd) 124 | func DeriveSymKey(pwd string) ([]byte, error) { 125 | if err := ValidatePassword(pwd); err != nil { 126 | return nil, fmt.Errorf("invalid password: %v", err) 127 | } 128 | 129 | return argon2.Key([]byte(pwd), nil, 1, 64*1024, 4, KeyLen), nil 130 | } 131 | 132 | // ProtectSymKey attempt to encrypt payload using given symmetric key 133 | func ProtectSymKey(payload, key []byte) ([]byte, error) { 134 | timestamp := make([]byte, TimestampLen) 135 | binary.LittleEndian.PutUint64(timestamp, uint64(time.Now().Unix())) 136 | 137 | ct, err := Encrypt(key, timestamp, payload) 138 | if err != nil { 139 | return nil, err 140 | } 141 | protected := append(timestamp, ct...) 142 | 143 | protectedLen := TimestampLen + len(payload) + TagLen 144 | if protectedLen != len(protected) { 145 | return nil, ErrInvalidProtectedLen 146 | } 147 | 148 | return protected, nil 149 | } 150 | 151 | // UnprotectSymKey attempt to decrypt protected bytes, using given symmetric key 152 | func UnprotectSymKey(protected, key []byte) ([]byte, error) { 153 | if len(protected) <= TimestampLen+TagLen { 154 | return nil, ErrTooShortCiphertext 155 | } 156 | 157 | ct := protected[TimestampLen:] 158 | timestamp := protected[:TimestampLen] 159 | 160 | if err := ValidateTimestamp(timestamp); err != nil { 161 | return nil, err 162 | } 163 | 164 | pt, err := Decrypt(key, timestamp, ct) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | return pt, nil 170 | } 171 | 172 | // RandomKey generates a random KeyLen-byte key usable by Encrypt and Decrypt 173 | func RandomKey() []byte { 174 | key := make([]byte, KeyLen) 175 | n, err := rand.Read(key) 176 | if err != nil { 177 | panic(err) 178 | } 179 | if n != KeyLen { 180 | panic(fmt.Errorf("bytes read mismatch in RandomKey: got %d wanted %d", n, KeyLen)) 181 | } 182 | 183 | return key 184 | } 185 | 186 | // RandomID generates a random IDLen-byte ID 187 | func RandomID() []byte { 188 | id := make([]byte, IDLen) 189 | n, err := rand.Read(id) 190 | if err != nil { 191 | panic(err) 192 | } 193 | if n != IDLen { 194 | panic(fmt.Errorf("bytes read mismatch in RandomID: got %d wanted %d", n, IDLen)) 195 | } 196 | 197 | return id 198 | } 199 | 200 | // RandomDelta16 produces a random 16-bit integer to allow us to 201 | // vary key sizes, plaintext sizes etc 202 | func RandomDelta16() uint16 { 203 | randAdjust := make([]byte, 2) 204 | rand.Read(randAdjust) 205 | return binary.LittleEndian.Uint16(randAdjust) 206 | } 207 | 208 | // Ed25519PrivateKeyFromPassword creates a ed25519.PrivateKey from a password 209 | func Ed25519PrivateKeyFromPassword(password string) (Ed25519PrivateKey, error) { 210 | if err := ValidatePassword(password); err != nil { 211 | return nil, fmt.Errorf("invalid password: %v", err) 212 | } 213 | 214 | seed := argon2.Key([]byte(password), nil, 1, 64*1024, 4, ed25519.SeedSize) 215 | return ed25519.NewKeyFromSeed(seed), nil 216 | } 217 | 218 | // PublicEd25519KeyToCurve25519 convert an Ed25519PublicKey to a Curve25519PublicKey. 219 | func PublicEd25519KeyToCurve25519(edPubKey Ed25519PublicKey) Curve25519PublicKey { 220 | var edPk [ed25519.PublicKeySize]byte 221 | var curveKey [Curve25519PubKeyLen]byte 222 | copy(edPk[:], edPubKey) 223 | if !extra25519.PublicKeyToCurve25519(&curveKey, &edPk) { 224 | panic("could not convert ed25519 public key to curve25519") 225 | } 226 | 227 | return curveKey[:] 228 | } 229 | 230 | // PrivateEd25519KeyToCurve25519 convert an Ed25519PrivateKey to a Curve25519PrivateKey. 231 | func PrivateEd25519KeyToCurve25519(edPrivKey Ed25519PrivateKey) Curve25519PrivateKey { 232 | var edSk [ed25519.PrivateKeySize]byte 233 | var curveKey [Curve25519PrivKeyLen]byte 234 | copy(edSk[:], edPrivKey) 235 | extra25519.PrivateKeyToCurve25519(&curveKey, &edSk) 236 | 237 | return curveKey[:] 238 | } 239 | -------------------------------------------------------------------------------- /crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package crypto 16 | 17 | import ( 18 | "bytes" 19 | "crypto/rand" 20 | "encoding/binary" 21 | "strings" 22 | "testing" 23 | "time" 24 | 25 | "github.com/teserakt-io/golang-ed25519/extra25519" 26 | "golang.org/x/crypto/ed25519" 27 | ) 28 | 29 | func TestRandomID(t *testing.T) { 30 | zeroID := make([]byte, IDLen) 31 | 32 | for i := 0; i < 256; i++ { 33 | randomID1 := RandomID() 34 | randomID2 := RandomID() 35 | 36 | if len(randomID1) != IDLen { 37 | t.Fatalf("Unexpected ID length, got %d, expected %d", len(randomID1), IDLen) 38 | } 39 | if len(randomID2) != IDLen { 40 | t.Fatalf("Unexpected ID length, got %d, expected %d", len(randomID2), IDLen) 41 | } 42 | 43 | if bytes.Equal(randomID1, zeroID) || bytes.Equal(randomID2, zeroID) { 44 | t.Fatal("Random ID is all zeros, not random") 45 | } 46 | if bytes.Equal(randomID1, randomID2) { 47 | t.Fatal("2 random IDs must not be equals") 48 | } 49 | } 50 | } 51 | 52 | // Test encrypt tests KATs for the encryption code 53 | func TestEncrypt(t *testing.T) { 54 | ptLen := 64 55 | adLen := 8 56 | 57 | key := make([]byte, KeyLen) 58 | pt := make([]byte, ptLen) 59 | ad := make([]byte, adLen) 60 | ctt := []byte{163, 170, 113, 22, 250, 77, 249, 210, 78, 28, 160, 45, 237, 93, 164, 200, 69, 177, 144, 88, 25, 34, 203, 0, 222, 9, 31, 200, 251, 127, 6, 91, 145, 230, 145, 187, 85, 154, 214, 154, 130, 152, 98, 74, 163, 29, 244, 187, 138, 58, 140, 254, 85, 107, 236, 245, 212, 233, 150, 187, 147, 172, 20, 22, 177, 76, 75, 137, 57, 249, 110, 197, 218, 174, 34, 208, 235, 228, 175, 83} 61 | 62 | for i := 0; i < KeyLen; i++ { 63 | key[i] = byte(i) 64 | } 65 | for i := 0; i < ptLen; i++ { 66 | pt[i] = byte(i) 67 | } 68 | for i := 0; i < adLen; i++ { 69 | ad[i] = byte(i) 70 | } 71 | ct, err := Encrypt(key, ad, pt) 72 | if err != nil { 73 | t.Fatalf("Encryption failed: %s", err) 74 | } 75 | if !bytes.Equal(ct, ctt) { 76 | t.Fatalf("Ciphertext doesn't match. Got %v, expected %v", ct, ctt) 77 | } 78 | } 79 | 80 | // TestRandom tests no trivial collisions exist, the correct 81 | // length of data is generated and that random does not generate 82 | // a zero key. 83 | // TODO: proper random testing? 84 | func TestRandomKey(t *testing.T) { 85 | zeroes := make([]byte, KeyLen) 86 | 87 | for i := 0; i < 256; i++ { 88 | k1 := RandomKey() 89 | k2 := RandomKey() 90 | 91 | if bytes.Equal(k1, k2) { 92 | t.Fatal("2 random keys must not be equals") 93 | } 94 | 95 | if len(k1) != KeyLen { 96 | t.Fatalf("Incorrect random key length, got %d, expected %d", len(k1), KeyLen) 97 | } 98 | if len(k2) != KeyLen { 99 | t.Fatalf("Incorrect random key length, got %d, expected %d", len(k2), KeyLen) 100 | } 101 | 102 | if bytes.Equal(k1, zeroes) || bytes.Equal(k2, zeroes) { 103 | t.Fatal("Random key is all zeros, not random") 104 | } 105 | } 106 | } 107 | 108 | // TestEncryptDecrypt tests that we can return the same plaintext as 109 | // we encrypted. In addition, it tests that modifications to 110 | // associated data, ciphertext or key produce a failure result. 111 | func TestEncryptDecrypt(t *testing.T) { 112 | for i := 0; i < 256; i++ { 113 | rDelta := RandomDelta16() 114 | 115 | ptLen := 1234 + rDelta 116 | 117 | key := make([]byte, KeyLen) 118 | ad := make([]byte, TimestampLen) 119 | pt := make([]byte, ptLen) 120 | 121 | rand.Read(key) 122 | rand.Read(ad) 123 | rand.Read(pt) 124 | 125 | ct, err := Encrypt(key, ad, pt) 126 | 127 | if err != nil { 128 | t.Fatalf("Encryption failed: %s", err) 129 | } 130 | if len(ct) != len(pt)+TagLen { 131 | t.Fatalf("Invalid ciphertext size: got: %d, wanted: %d", len(ct), len(pt)+TagLen) 132 | } 133 | 134 | // happy case: 135 | ptt, err := Decrypt(key, ad, ct) 136 | if err != nil { 137 | t.Fatalf("Decryption failed: %v", err) 138 | } 139 | if len(ptt) != len(pt) { 140 | t.Fatalf("Decrypted message has different length than original: got: %d, wanted: %d", len(ptt), len(pt)) 141 | } 142 | 143 | if !bytes.Equal(ptt, pt) { 144 | t.Fatalf("Invalid decrypted message, got %v, wanted %v", ptt, pt) 145 | } 146 | 147 | // invalid ad: 148 | adInvalid := make([]byte, TimestampLen) 149 | copy(adInvalid, ad) 150 | for i := range adInvalid { 151 | adInvalid[i] ^= 0x01 152 | } 153 | 154 | _, err = Decrypt(key, adInvalid, ct) 155 | if err == nil { 156 | t.Fatal("Expected a decryption error with an invalid ad.") 157 | } 158 | 159 | // invalid ciphertext 160 | ctLength := len(ct) 161 | ctInvalid := make([]byte, ctLength) 162 | copy(ctInvalid, ct) 163 | for i := range ctInvalid { 164 | ctInvalid[i] ^= 0x01 165 | } 166 | _, err = Decrypt(key, ad, ctInvalid) 167 | if err == nil { 168 | t.Fatal("Expected a decryption error with an invalid ct.") 169 | } 170 | 171 | // invalid key should obviously not work either 172 | zeroKey := make([]byte, KeyLen) 173 | _, err = Decrypt(zeroKey, ad, ct) 174 | if err == nil { 175 | t.Fatal("Expected a decryption error with an invalid key.") 176 | } 177 | 178 | // truncated/too short ciphertext 179 | truncatedCt := ct[:2] 180 | _, err = Decrypt(key, ad, truncatedCt) 181 | if err == nil { 182 | t.Fatal("Expected a decryption error with a truncated ct.") 183 | } 184 | } 185 | } 186 | 187 | func TestEncryptInvalidKeys(t *testing.T) { 188 | key := make([]byte, KeyLen) 189 | _, err := Encrypt(key, nil, nil) 190 | if err == nil { 191 | t.Fatal("Expected an error when calling encrypt with zero key") 192 | } 193 | 194 | _, err = Encrypt(key[:len(key)-1], nil, nil) 195 | if err == nil { 196 | t.Fatal("Expected an error when calling encrypt with a too short key") 197 | } 198 | } 199 | 200 | func TestProtectUnprotectSymKey(t *testing.T) { 201 | payload := []byte("some test payload") 202 | key := RandomKey() 203 | 204 | protected, err := ProtectSymKey(payload, key) 205 | if err != nil { 206 | t.Fatalf("ProtectSymKey failed: %v", err) 207 | } 208 | 209 | unprotected, err := UnprotectSymKey(protected, key) 210 | if err != nil { 211 | t.Fatalf("UnprotectSymKey failed: %v", err) 212 | } 213 | 214 | if !bytes.Equal(unprotected, payload) { 215 | t.Fatalf("Invalid unprotected payload: got: %v, wanted: %v", unprotected, payload) 216 | } 217 | 218 | now := time.Now() 219 | timestamp := make([]byte, TimestampLen) 220 | 221 | // Replace timestamp in cipher by a too old timestamp 222 | pastTs := now.Add(time.Duration(-MaxDelayDuration)) 223 | binary.LittleEndian.PutUint64(timestamp, uint64(pastTs.Unix())) 224 | tooOldProtected := append(timestamp, protected[TimestampLen:]...) 225 | _, err = UnprotectSymKey(tooOldProtected, key) 226 | if err != ErrTimestampTooOld { 227 | t.Fatalf("Invalid error, got: %v, wanted: %v", err, ErrTimestampTooOld) 228 | } 229 | 230 | // Replace timestamp in cipher by a timestamp in future 231 | futureTs := now.Add(1 * time.Second) 232 | binary.LittleEndian.PutUint64(timestamp, uint64(futureTs.Unix())) 233 | futureProtected := append(timestamp, protected[TimestampLen:]...) 234 | _, err = UnprotectSymKey(futureProtected, key) 235 | if err != ErrTimestampInFuture { 236 | t.Fatalf("Invalid error, got: %v, wanted: %v", err, ErrTimestampInFuture) 237 | } 238 | 239 | // Too short cipher are not allowed 240 | tooShortProtected := make([]byte, TimestampLen) 241 | _, err = UnprotectSymKey(tooShortProtected, key) 242 | if err != ErrTooShortCiphertext { 243 | t.Fatalf("Invalid error, got: %v, wanted: %v", err, ErrTooShortCiphertext) 244 | } 245 | 246 | if _, err := UnprotectSymKey(protected, []byte("not a key")); err == nil { 247 | t.Fatal("Expected unprotectSymKey to fail with an invalid key") 248 | } 249 | 250 | if _, err := ProtectSymKey([]byte("message"), []byte("not a key")); err == nil { 251 | t.Fatal("Expected protectSymKey to fail with an invalid key") 252 | } 253 | } 254 | 255 | func TestEd25519PrivateKeyFromPassword(t *testing.T) { 256 | password := "some random password" 257 | expectedKey := []byte{ 258 | 0xb7, 0x5a, 0x20, 0xc3, 0x9f, 0xeb, 0x46, 0xd1, 0x89, 0xa8, 0x78, 0x4e, 0xda, 0x1a, 0x36, 0x6a, 0xa3, 0xea, 0x8d, 259 | 0xf4, 0x4f, 0xc5, 0xb7, 0xfd, 0x63, 0x4d, 0xa4, 0xd7, 0xe4, 0xaf, 0x98, 0xbe, 0x4f, 0x2e, 0x32, 0xfa, 0xdf, 0xc2, 260 | 0xb2, 0xab, 0x98, 0x2f, 0xd7, 0xc, 0xb0, 0xfa, 0x3b, 0x98, 0x5f, 0x71, 0x8, 0x14, 0x56, 0x9c, 0x73, 0xfe, 0xd8, 261 | 0x67, 0x82, 0xf2, 0xd5, 0x29, 0x73, 0x58, 262 | } 263 | 264 | key, err := Ed25519PrivateKeyFromPassword(password) 265 | if err != nil { 266 | t.Fatalf("Failed to create private key from password: %v", err) 267 | } 268 | 269 | if !bytes.Equal(expectedKey, key) { 270 | t.Fatalf("Invalid key, got: %v, wanted: %v", key, expectedKey) 271 | } 272 | 273 | _, err = Ed25519PrivateKeyFromPassword(strings.Repeat("a", PasswordMinLength-1)) 274 | if err == nil { 275 | t.Fatal("Expected an error with a too short password") 276 | } 277 | } 278 | 279 | func TestDeriveSymKey(t *testing.T) { 280 | _, err := DeriveSymKey(strings.Repeat("a", PasswordMinLength-1)) 281 | if err == nil { 282 | t.Fatal("Expected an error with too short password") 283 | } 284 | 285 | k, err := DeriveSymKey("testPasswordRandom") 286 | if err != nil { 287 | t.Fatalf("DeriveSymKey error, got: %v, wanted nil", err) 288 | } 289 | 290 | if len(k) != KeyLen { 291 | t.Fatalf("Invalid key length: got: %d, wanted: %d", len(k), KeyLen) 292 | } 293 | } 294 | 295 | func TestPublicEd25519KeyToCurve25519(t *testing.T) { 296 | pubKey, _, err := ed25519.GenerateKey(rand.Reader) 297 | if err != nil { 298 | t.Fatalf("Failed to generate ed25519 key: %v", err) 299 | } 300 | 301 | var pk [ed25519.PublicKeySize]byte 302 | copy(pk[:], pubKey) 303 | 304 | var expectedCurveKey [Curve25519PubKeyLen]byte 305 | extra25519.PublicKeyToCurve25519(&expectedCurveKey, &pk) 306 | 307 | curveKey := PublicEd25519KeyToCurve25519(pubKey) 308 | if !bytes.Equal(curveKey, expectedCurveKey[:]) { 309 | t.Fatalf("Invalid curveKey, got %x, wanted %x", curveKey, expectedCurveKey) 310 | } 311 | } 312 | 313 | func TestPrivateEd25519KeyToCurve25519(t *testing.T) { 314 | _, privKey, err := ed25519.GenerateKey(rand.Reader) 315 | if err != nil { 316 | t.Fatalf("Failed to generate ed25519 key: %v", err) 317 | } 318 | 319 | var sk [64]byte 320 | copy(sk[:], privKey) 321 | 322 | var expectedCurveKey [Curve25519PrivKeyLen]byte 323 | extra25519.PrivateKeyToCurve25519(&expectedCurveKey, &sk) 324 | 325 | curveKey := PrivateEd25519KeyToCurve25519(privKey) 326 | if !bytes.Equal(curveKey, expectedCurveKey[:]) { 327 | t.Fatalf("Invalid curveKey, got %x, wanted %x", curveKey, expectedCurveKey) 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /crypto/hash.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package crypto 16 | 17 | import "golang.org/x/crypto/sha3" 18 | 19 | // Sha3Sum256 returns the sha3 sum of given data 20 | func Sha3Sum256(data []byte) []byte { 21 | h := sha3.Sum256(data) 22 | return h[:] 23 | } 24 | 25 | // HashTopic creates a topic hash from a topic string 26 | func HashTopic(topic string) []byte { 27 | return Sha3Sum256([]byte(topic))[:HashLen] 28 | } 29 | 30 | // HashIDAlias creates an ID from an ID alias string 31 | func HashIDAlias(idalias string) []byte { 32 | return Sha3Sum256([]byte(idalias))[:IDLen] 33 | } 34 | -------------------------------------------------------------------------------- /crypto/hash_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package crypto 16 | 17 | import ( 18 | "encoding/hex" 19 | "testing" 20 | ) 21 | 22 | /* TestHash tests KATs for both the hash function of choice and 23 | * the password hashing function / KDF of choice */ 24 | func TestHash(t *testing.T) { 25 | h := hex.EncodeToString(HashIDAlias("abc")) 26 | expected := "3a985da74fe225b2045c172d6bd390bd" 27 | if h != expected { 28 | t.Fatalf("Hash of ID alias incorrect, got: %s, wanted: %s", h, expected) 29 | } 30 | 31 | k, err := DeriveSymKey("testRandomPassword") 32 | if err != nil { 33 | t.Fatalf("Failed to derive symkey: %v", err) 34 | } 35 | 36 | h = hex.EncodeToString(k) 37 | expected = "ae153aa9dad7a10b0aed6d5bcfb407c77066acfbb2eaa702a6a88b6cf1b88c33" 38 | if h != expected { 39 | t.Fatalf("Hash of password incorrect, got: %s, wanted: %s", h, expected) 40 | } 41 | 42 | h = hex.EncodeToString(HashTopic("abc")) 43 | expected = "3a985da74fe225b2045c172d6bd390bd" 44 | if h != expected { 45 | t.Fatalf("Hash of Topic incorrect, got: %s, wanted: %s", h, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crypto/validators.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package crypto 16 | 17 | import ( 18 | "bytes" 19 | "encoding/binary" 20 | "errors" 21 | "fmt" 22 | "time" 23 | "unicode/utf8" 24 | 25 | "golang.org/x/crypto/ed25519" 26 | ) 27 | 28 | const ( 29 | // PasswordMinLength defines the minimum size accepted for a password 30 | PasswordMinLength = 16 31 | // NameMinLen is the minimum length of a name 32 | NameMinLen = 1 33 | // NameMaxLen is the maximum length of a name 34 | NameMaxLen = 255 35 | ) 36 | 37 | var ( 38 | blankEd25519pk [ed25519.PublicKeySize]byte 39 | zeroEd25519pk = blankEd25519pk[:] 40 | blankEd25519sk [ed25519.PrivateKeySize]byte 41 | zeroEd25519sk = blankEd25519sk[:] 42 | 43 | blankCurve25519pk [Curve25519PubKeyLen]byte 44 | blankCurve25519sk [Curve25519PrivKeyLen]byte 45 | zeroCurve25519pk = blankCurve25519pk[:] 46 | zeroCurve25519sk = blankCurve25519sk[:] 47 | 48 | blankSymKey [KeyLen]byte 49 | zeroSymKey = blankSymKey[:] 50 | ) 51 | 52 | // ValidateSymKey checks that a key is of the expected length 53 | // and not filled with zero 54 | func ValidateSymKey(key []byte) error { 55 | if g, w := len(key), KeyLen; g != w { 56 | return fmt.Errorf("invalid symmetric key length, got %d, expected %d", g, w) 57 | } 58 | 59 | if bytes.Equal(zeroSymKey, key) { 60 | return errors.New("invalid symmetric key, all zeros") 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // ValidateEd25519PrivKey checks that a key is of the expected length and not all zero 67 | func ValidateEd25519PrivKey(key []byte) error { 68 | if g, w := len(key), ed25519.PrivateKeySize; g != w { 69 | return fmt.Errorf("invalid private key length, got %d, expected %d", g, w) 70 | } 71 | 72 | if bytes.Equal(zeroEd25519sk, key) { 73 | return errors.New("invalid private key, all zeros") 74 | } 75 | 76 | return nil 77 | } 78 | 79 | // ValidateEd25519PubKey checks that a key is of the expected length and not all zero 80 | func ValidateEd25519PubKey(key []byte) error { 81 | if g, w := len(key), ed25519.PublicKeySize; g != w { 82 | return fmt.Errorf("invalid public key length, got %d, expected %d", g, w) 83 | } 84 | 85 | if bytes.Equal(zeroEd25519pk, key) { 86 | return errors.New("invalid public key, all zeros") 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // ValidateCurve25519PubKey checks that a key is of the expected length and not all zero 93 | func ValidateCurve25519PubKey(key []byte) error { 94 | if g, w := len(key), Curve25519PubKeyLen; g != w { 95 | return fmt.Errorf("invalid public key length, got %d, expected %d", g, w) 96 | } 97 | 98 | if bytes.Equal(zeroCurve25519pk, key) { 99 | return errors.New("invalid public key, all zeros") 100 | } 101 | 102 | return nil 103 | } 104 | 105 | // ValidateCurve25519PrivKey checks that a key is of the expected length and not all zero 106 | func ValidateCurve25519PrivKey(key []byte) error { 107 | if g, w := len(key), Curve25519PrivKeyLen; g != w { 108 | return fmt.Errorf("invalid private key length, got %d, expected %d", g, w) 109 | } 110 | 111 | if bytes.Equal(zeroCurve25519sk, key) { 112 | return errors.New("invalid private key, all zeros") 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // ValidateID checks that an id is of the expected length 119 | func ValidateID(id []byte) error { 120 | if g, w := len(id), IDLen; g != w { 121 | return fmt.Errorf("invalid ID length, got %d, expected %d", g, w) 122 | } 123 | 124 | return nil 125 | } 126 | 127 | // ValidateName is used to validate names match given constraints 128 | // since we hash these in the protocol, those constraints are quite 129 | // liberal, but for correctness we check any string is valid UTF-8 130 | func ValidateName(name string) error { 131 | if !utf8.ValidString(name) { 132 | return fmt.Errorf("name is not a valid UTF-8 string") 133 | } 134 | 135 | namelen := len(name) 136 | if namelen < NameMinLen || namelen > NameMaxLen { 137 | return fmt.Errorf("name length is invalid, names are between %d and %d characters", NameMinLen, NameMaxLen) 138 | } 139 | 140 | return nil 141 | } 142 | 143 | // ValidateTopic checks if a topic is not too large or empty 144 | func ValidateTopic(topic string) error { 145 | if len(topic) > MaxTopicLen { 146 | return fmt.Errorf("topic too long, expected %d chars maximum, got %d", MaxTopicLen, len(topic)) 147 | } 148 | 149 | if len(topic) == 0 { 150 | return errors.New("topic cannot be empty") 151 | } 152 | 153 | return nil 154 | } 155 | 156 | // ValidateTopicHash checks that a topic hash is of the expected length 157 | func ValidateTopicHash(topicHash []byte) error { 158 | if g, w := len(topicHash), HashLen; g != w { 159 | return fmt.Errorf("invalid Topic Hash length, got %d, expected %d", g, w) 160 | } 161 | 162 | return nil 163 | } 164 | 165 | // ValidateTimestamp checks that given timestamp bytes are 166 | // a valid LittleEndian encoded timestamp, not in the future and not older than MaxDelayDuration 167 | func ValidateTimestamp(timestamp []byte) error { 168 | now := time.Now() 169 | tsTime := time.Unix(int64(binary.LittleEndian.Uint64(timestamp)), 0) 170 | 171 | if now.Before(tsTime) { 172 | return ErrTimestampInFuture 173 | } 174 | 175 | leastValidTime := now.Add(-MaxDelayDuration) 176 | if leastValidTime.After(tsTime) { 177 | return ErrTimestampTooOld 178 | } 179 | 180 | return nil 181 | } 182 | 183 | // ValidateTimestampKey checks that given timestamp bytes are 184 | // a valid LittleEndian encoded timestamp, not in the future and not older than MaxDelayKeyTransition 185 | func ValidateTimestampKey(timestamp []byte) error { 186 | now := time.Now() 187 | tsTime := time.Unix(int64(binary.LittleEndian.Uint64(timestamp)), 0) 188 | if now.Before(tsTime) { 189 | return ErrTimestampInFuture 190 | } 191 | 192 | leastValidTime := now.Add(-MaxDelayKeyTransition) 193 | if leastValidTime.After(tsTime) { 194 | return ErrTimestampTooOld 195 | } 196 | 197 | return nil 198 | } 199 | 200 | // ValidatePassword checks given password is an utf8 string of at least PasswordMinLength characters 201 | func ValidatePassword(password string) error { 202 | if !utf8.ValidString(password) { 203 | return errors.New("password is not a valid UTF-8 string") 204 | } 205 | 206 | if len(password) < PasswordMinLength { 207 | return fmt.Errorf("password must be at least %d characters", PasswordMinLength) 208 | } 209 | 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /crypto/validators_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package crypto 16 | 17 | import ( 18 | "crypto/rand" 19 | "encoding/binary" 20 | "strings" 21 | "testing" 22 | "time" 23 | 24 | "golang.org/x/crypto/ed25519" 25 | ) 26 | 27 | func TestValidateName(t *testing.T) { 28 | t.Run("Invalid names return errors", func(t *testing.T) { 29 | invalidNames := []string{ 30 | "", 31 | string([]byte{0xfe}), 32 | string([]byte{0xff}), 33 | string([]byte{0xf8, 0x80, 0x80, 0x80}), 34 | } 35 | for _, invalidName := range invalidNames { 36 | if err := ValidateName(invalidName); err == nil { 37 | t.Fatalf("Expected name '%s' validation to return an error", invalidName) 38 | } 39 | } 40 | }) 41 | 42 | t.Run("Valid names return no error", func(t *testing.T) { 43 | validNames := []string{ 44 | "randomName", 45 | "исследование", 46 | "研究", 47 | } 48 | for _, validName := range validNames { 49 | if err := ValidateName(validName); err != nil { 50 | t.Fatalf("Got error %v when validating name '%s', wanted no error", err, validName) 51 | } 52 | } 53 | }) 54 | } 55 | 56 | func TestValidateID(t *testing.T) { 57 | t.Run("Invalid ids return an error", func(t *testing.T) { 58 | invalidIDs := [][]byte{ 59 | nil, 60 | []byte{}, 61 | make([]byte, IDLen-1), 62 | make([]byte, IDLen+1), 63 | } 64 | 65 | for _, invalidID := range invalidIDs { 66 | if err := ValidateID(invalidID); err == nil { 67 | t.Fatalf("Expected id '%v' validation to return an error", invalidID) 68 | } 69 | } 70 | }) 71 | 72 | t.Run("Valid ids return no error", func(t *testing.T) { 73 | validIDs := [][]byte{ 74 | make([]byte, IDLen), 75 | } 76 | 77 | for _, validID := range validIDs { 78 | if err := ValidateID(validID); err != nil { 79 | t.Fatalf("Got validation error %v when validating ID '%v', wanted no error", err, validID) 80 | } 81 | } 82 | }) 83 | } 84 | 85 | func TestValidateEd25519PrivKey(t *testing.T) { 86 | t.Run("Invalid private keys return an error", func(t *testing.T) { 87 | allZeroKey := make(ed25519.PrivateKey, ed25519.PrivateKeySize) 88 | 89 | tooLongKey := make(ed25519.PrivateKey, ed25519.PrivateKeySize+1) 90 | rand.Read(tooLongKey) 91 | 92 | tooShortKey := make(ed25519.PrivateKey, ed25519.PrivateKeySize-1) 93 | rand.Read(tooShortKey) 94 | 95 | invalidKeys := []ed25519.PrivateKey{ 96 | nil, 97 | allZeroKey, 98 | tooLongKey, 99 | tooShortKey, 100 | } 101 | 102 | for _, invalidKey := range invalidKeys { 103 | if err := ValidateEd25519PrivKey(invalidKey); err == nil { 104 | t.Fatalf("Expected key '%v' validation to return an error", invalidKey) 105 | } 106 | } 107 | }) 108 | 109 | t.Run("Valid private keys return no error", func(t *testing.T) { 110 | validKey := make(ed25519.PrivateKey, ed25519.PrivateKeySize) 111 | rand.Read(validKey) 112 | 113 | validKeys := [][]byte{ 114 | validKey, 115 | } 116 | 117 | for _, validKey := range validKeys { 118 | if err := ValidateEd25519PrivKey(validKey); err != nil { 119 | t.Fatalf("Got error %v when validating key '%v', wanted no error", err, validKey) 120 | } 121 | } 122 | }) 123 | } 124 | 125 | func TestValidEd25519PubKey(t *testing.T) { 126 | t.Run("Invalid public keys return an error", func(t *testing.T) { 127 | allZeroKey := make(ed25519.PublicKey, ed25519.PublicKeySize) 128 | 129 | tooLongKey := make(ed25519.PublicKey, ed25519.PublicKeySize+1) 130 | rand.Read(tooLongKey) 131 | 132 | tooShortKey := make(ed25519.PublicKey, ed25519.PublicKeySize-1) 133 | rand.Read(tooShortKey) 134 | 135 | invalidKeys := []ed25519.PublicKey{ 136 | allZeroKey, 137 | tooLongKey, 138 | tooShortKey, 139 | } 140 | 141 | for _, invalidKey := range invalidKeys { 142 | if err := ValidateEd25519PubKey(invalidKey); err == nil { 143 | t.Fatalf("Expected key '%v' validation to return an error", invalidKey) 144 | } 145 | } 146 | }) 147 | 148 | t.Run("Valid public keys return no error", func(t *testing.T) { 149 | validKey := make(ed25519.PublicKey, ed25519.PublicKeySize) 150 | rand.Read(validKey) 151 | 152 | validKeys := [][]byte{ 153 | validKey, 154 | } 155 | 156 | for _, validKey := range validKeys { 157 | if err := ValidateEd25519PubKey(validKey); err != nil { 158 | t.Fatalf("Got error %v when validating key '%v', wanted no error", err, validKey) 159 | } 160 | } 161 | }) 162 | } 163 | 164 | func TestValidateTopic(t *testing.T) { 165 | t.Run("Invalid topics return an error", func(t *testing.T) { 166 | invalidTopics := []string{ 167 | "", 168 | strings.Repeat("a", MaxTopicLen+1), 169 | } 170 | 171 | for _, invalidTopic := range invalidTopics { 172 | if err := ValidateTopic(invalidTopic); err == nil { 173 | t.Fatalf("Expected topic '%v' validation to return an error", invalidTopic) 174 | } 175 | } 176 | }) 177 | 178 | t.Run("Valid topics return no error", func(t *testing.T) { 179 | validTopics := []string{ 180 | strings.Repeat("a", MaxTopicLen), 181 | "a", 182 | "/some/topic", 183 | } 184 | 185 | for _, validTopic := range validTopics { 186 | if err := ValidateTopic(validTopic); err != nil { 187 | t.Fatalf("Got error %v when validating topic '%v', wanted no error", err, validTopic) 188 | } 189 | } 190 | }) 191 | } 192 | 193 | func TestValidateTopicHash(t *testing.T) { 194 | t.Run("Invalid topic hashes return an error", func(t *testing.T) { 195 | tooShortHash := make([]byte, HashLen-1) 196 | tooLongHash := make([]byte, HashLen+1) 197 | 198 | invalidTopics := [][]byte{ 199 | tooShortHash, 200 | tooLongHash, 201 | } 202 | 203 | for _, invalidTopic := range invalidTopics { 204 | if err := ValidateTopicHash(invalidTopic); err == nil { 205 | t.Fatalf("Expected topic '%v' validation to return an error", invalidTopic) 206 | } 207 | } 208 | }) 209 | 210 | t.Run("Valid topic hashes return no error", func(t *testing.T) { 211 | allZeroHash := make([]byte, HashLen) 212 | 213 | randomHash := make([]byte, HashLen) 214 | rand.Read(randomHash) 215 | 216 | validTopics := [][]byte{ 217 | allZeroHash, 218 | randomHash, 219 | } 220 | 221 | for _, validTopic := range validTopics { 222 | if err := ValidateTopicHash(validTopic); err != nil { 223 | t.Fatalf("Got error %v when validating topic hash '%v', wanted no error", err, validTopic) 224 | } 225 | } 226 | }) 227 | } 228 | 229 | func TestValidateTimestamp(t *testing.T) { 230 | futureTimestamp := make([]byte, TimestampLen) 231 | binary.LittleEndian.PutUint64(futureTimestamp, uint64(time.Now().Add(1*time.Second).Unix())) 232 | if err := ValidateTimestamp(futureTimestamp); err != ErrTimestampInFuture { 233 | t.Fatalf("Expected timestamp in the future to not be valid: got %v, wanted %v", err, ErrTimestampInFuture) 234 | } 235 | 236 | pastTimestamp := make([]byte, TimestampLen) 237 | binary.LittleEndian.PutUint64(pastTimestamp, uint64(time.Now().Add(-(MaxDelayDuration + 1)).Unix())) 238 | if err := ValidateTimestamp(pastTimestamp); err != ErrTimestampTooOld { 239 | t.Fatalf("Expected timestamp too far in past to not be valid") 240 | } 241 | 242 | validTimestamp := make([]byte, TimestampLen) 243 | binary.LittleEndian.PutUint64(validTimestamp, uint64(time.Now().Unix())) 244 | if err := ValidateTimestamp(validTimestamp); err != nil { 245 | t.Fatalf("Got error %v when validating timestamp %v, wanted no error", err, validTimestamp) 246 | } 247 | } 248 | 249 | func TestValidateTimestampKey(t *testing.T) { 250 | futureTimestamp := make([]byte, TimestampLen) 251 | binary.LittleEndian.PutUint64(futureTimestamp, uint64(time.Now().Add(1*time.Second).Unix())) 252 | if err := ValidateTimestampKey(futureTimestamp); err != ErrTimestampInFuture { 253 | t.Fatalf("Expected timestamp in the future to not be valid: got %v, wanted %v", err, ErrTimestampInFuture) 254 | } 255 | 256 | pastTimestamp := make([]byte, TimestampLen) 257 | binary.LittleEndian.PutUint64(pastTimestamp, uint64(time.Now().Add(-(MaxDelayKeyTransition + 1)).Unix())) 258 | if err := ValidateTimestampKey(pastTimestamp); err != ErrTimestampTooOld { 259 | t.Fatalf("Expected timestamp too far in past to not be valid: got %v, wanted %v", err, ErrTimestampTooOld) 260 | } 261 | 262 | validTimestamp := make([]byte, TimestampLen) 263 | binary.LittleEndian.PutUint64(validTimestamp, uint64(time.Now().Unix())) 264 | if err := ValidateTimestampKey(validTimestamp); err != nil { 265 | t.Fatalf("Got error %v when validating timestamp %v, wanted no error", err, validTimestamp) 266 | } 267 | } 268 | 269 | func TestValidateCurve25519PubKey(t *testing.T) { 270 | t.Run("Invalid public keys return an error", func(t *testing.T) { 271 | allZeroKey := make([]byte, Curve25519PubKeyLen) 272 | 273 | tooLongKey := make([]byte, Curve25519PubKeyLen+1) 274 | rand.Read(tooLongKey) 275 | 276 | tooShortKey := make([]byte, Curve25519PubKeyLen-1) 277 | rand.Read(tooShortKey) 278 | 279 | invalidKeys := [][]byte{ 280 | allZeroKey, 281 | tooLongKey, 282 | tooShortKey, 283 | } 284 | 285 | for _, invalidKey := range invalidKeys { 286 | if err := ValidateCurve25519PubKey(invalidKey); err == nil { 287 | t.Fatalf("Expected key '%v' validation to return an error", invalidKey) 288 | } 289 | } 290 | }) 291 | 292 | t.Run("Valid public keys return no error", func(t *testing.T) { 293 | validKey := make([]byte, Curve25519PubKeyLen) 294 | rand.Read(validKey) 295 | 296 | validKeys := [][]byte{ 297 | validKey, 298 | } 299 | 300 | for _, validKey := range validKeys { 301 | if err := ValidateCurve25519PubKey(validKey); err != nil { 302 | t.Fatalf("Got error %v when validating key '%v', wanted no error", err, validKey) 303 | } 304 | } 305 | }) 306 | } 307 | 308 | func TestValidateCurve25519PrivKey(t *testing.T) { 309 | t.Run("Invalid private keys return an error", func(t *testing.T) { 310 | allZeroKey := make([]byte, Curve25519PrivKeyLen) 311 | 312 | tooLongKey := make([]byte, Curve25519PrivKeyLen+1) 313 | rand.Read(tooLongKey) 314 | 315 | tooShortKey := make([]byte, Curve25519PrivKeyLen-1) 316 | rand.Read(tooShortKey) 317 | 318 | invalidKeys := [][]byte{ 319 | allZeroKey, 320 | tooLongKey, 321 | tooShortKey, 322 | } 323 | 324 | for _, invalidKey := range invalidKeys { 325 | if err := ValidateCurve25519PrivKey(invalidKey); err == nil { 326 | t.Fatalf("Expected key '%v' validation to return an error", invalidKey) 327 | } 328 | } 329 | }) 330 | 331 | t.Run("Valid private keys return no error", func(t *testing.T) { 332 | validKey := make([]byte, Curve25519PrivKeyLen) 333 | rand.Read(validKey) 334 | 335 | validKeys := [][]byte{ 336 | validKey, 337 | } 338 | 339 | for _, validKey := range validKeys { 340 | if err := ValidateCurve25519PrivKey(validKey); err != nil { 341 | t.Fatalf("Got error %v when validating key '%v', wanted no error", err, validKey) 342 | } 343 | } 344 | }) 345 | } 346 | 347 | func TestValidatePassword(t *testing.T) { 348 | t.Run("Invalid passwords return errors", func(t *testing.T) { 349 | invalidPasswords := []string{ 350 | "", 351 | string([]byte{0xfe}), 352 | string([]byte{0xff}), 353 | string([]byte{0xf8, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}), 354 | strings.Repeat("a", PasswordMinLength-1), 355 | } 356 | for _, invalidPassword := range invalidPasswords { 357 | if err := ValidatePassword(invalidPassword); err == nil { 358 | t.Fatalf("Expected password '%s' validation to return an error", invalidPassword) 359 | } 360 | } 361 | }) 362 | 363 | t.Run("Valid passwords return no error", func(t *testing.T) { 364 | validPasswords := []string{ 365 | strings.Repeat("a", PasswordMinLength), 366 | "исследованиеание", 367 | "研究研究研究研究", 368 | } 369 | for _, validPassword := range validPasswords { 370 | if err := ValidatePassword(validPassword); err != nil { 371 | t.Fatalf("Got error %v when validating password '%s', wanted no error", err, validPassword) 372 | } 373 | } 374 | }) 375 | } 376 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e4_test 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | e4 "github.com/teserakt-io/e4go" 22 | e4crypto "github.com/teserakt-io/e4go/crypto" 23 | "golang.org/x/crypto/curve25519" 24 | ) 25 | 26 | func ExampleNewClient_symIDAndKey() { 27 | client, err := e4.NewClient(&e4.SymIDAndKey{ 28 | ID: []byte("clientID"), 29 | Key: e4crypto.RandomKey(), 30 | }, e4.NewInMemoryStore(nil)) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | protectedMessage, err := client.ProtectMessage([]byte("very secret message"), "topic/name") 36 | if err != nil { 37 | panic(err) 38 | } 39 | fmt.Printf("Protected message: %v", protectedMessage) 40 | } 41 | 42 | func ExampleNewClient_fileStorage() { 43 | f, err := os.OpenFile("/storage/clientID.json", os.O_CREATE|os.O_RDWR, 0600) 44 | if err != nil { 45 | panic(err) 46 | } 47 | defer f.Close() 48 | 49 | client, err := e4.NewClient(&e4.SymIDAndKey{ 50 | ID: []byte("clientID"), 51 | Key: e4crypto.RandomKey(), 52 | }, f) 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | protectedMessage, err := client.ProtectMessage([]byte("very secret message"), "topic/name") 58 | if err != nil { 59 | panic(err) 60 | } 61 | fmt.Printf("Protected message: %v", protectedMessage) 62 | } 63 | 64 | func ExampleNewClient_symNameAndPassword() { 65 | client, err := e4.NewClient(&e4.SymNameAndPassword{ 66 | Name: "clientName", 67 | Password: "verySecretPassword", 68 | }, e4.NewInMemoryStore(nil)) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | protectedMessage, err := client.ProtectMessage([]byte("very secret message"), "topic/name") 74 | if err != nil { 75 | panic(err) 76 | } 77 | fmt.Printf("Protected message: %v", protectedMessage) 78 | } 79 | 80 | func ExampleNewClient_pubIDAndKey() { 81 | privateKey, err := e4crypto.Ed25519PrivateKeyFromPassword("verySecretPassword") 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | c2PubKey, err := curve25519.X25519(e4crypto.RandomKey(), curve25519.Basepoint) 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | client, err := e4.NewClient(&e4.PubIDAndKey{ 92 | ID: []byte("clientID"), 93 | Key: privateKey, 94 | C2PubKey: c2PubKey, 95 | }, e4.NewInMemoryStore(nil)) 96 | 97 | if err != nil { 98 | panic(err) 99 | } 100 | 101 | protectedMessage, err := client.ProtectMessage([]byte("very secret message"), "topic/name") 102 | if err != nil { 103 | panic(err) 104 | } 105 | fmt.Printf("Protected message: %v", protectedMessage) 106 | } 107 | 108 | func ExampleNewClient_pubNameAndPassword() { 109 | c2PubKey, err := curve25519.X25519(e4crypto.RandomKey(), curve25519.Basepoint) 110 | if err != nil { 111 | panic(err) 112 | } 113 | 114 | config := &e4.PubNameAndPassword{ 115 | Name: "clientName", 116 | Password: "verySecretPassword", 117 | C2PubKey: c2PubKey, 118 | } 119 | client, err := e4.NewClient(config, e4.NewInMemoryStore(nil)) 120 | if err != nil { 121 | panic(err) 122 | } 123 | 124 | // We may need to get the public key derived from the password: 125 | pubKey, err := config.PubKey() 126 | if err != nil { 127 | panic(err) 128 | } 129 | fmt.Printf("Client public key: %x", pubKey) 130 | 131 | protectedMessage, err := client.ProtectMessage([]byte("very secret message"), "topic/name") 132 | if err != nil { 133 | panic(err) 134 | } 135 | fmt.Printf("Protected message: %v", protectedMessage) 136 | } 137 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/teserakt-io/e4go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/eclipse/paho.mqtt.golang v1.2.0 7 | github.com/google/go-cmp v0.3.0 // indirect 8 | github.com/marcusolsson/tui-go v0.4.0 9 | github.com/miscreant/miscreant.go v0.0.0-20190903041724-6bebe170cbaf 10 | github.com/teserakt-io/golang-ed25519 v0.0.0-20200315192543-8255be791ce4 11 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 12 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect 13 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b // indirect 14 | golang.org/x/text v0.3.2 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= 2 | github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= 3 | github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI= 4 | github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ= 5 | github.com/gdamore/tcell v1.1.0 h1:RbQgl7jukmdqROeNcKps7R2YfDCQbWkOd1BwdXrxfr4= 6 | github.com/gdamore/tcell v1.1.0/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A= 7 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 8 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 9 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 10 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 11 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= 12 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 13 | github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= 14 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 15 | github.com/lucasb-eyer/go-colorful v0.0.0-20180709185858-c7842319cf3a h1:B2QfFRl5yGVGGcyEVFzfdXlC1BBvszsIAsCeef2oD0k= 16 | github.com/lucasb-eyer/go-colorful v0.0.0-20180709185858-c7842319cf3a/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= 17 | github.com/marcusolsson/tui-go v0.4.0 h1:PZD0lIS+2OUKxs71qsc5U/P+eVU39FeBRgdsh5iQZ28= 18 | github.com/marcusolsson/tui-go v0.4.0/go.mod h1:vp1U15jwzYTPWex1hV+CZ7MeQQH7Wr73fz9hc/0I9YI= 19 | github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= 20 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 21 | github.com/miscreant/miscreant.go v0.0.0-20190903041724-6bebe170cbaf h1:WHuYoA2VHEGYtfIaV2oBwa8csNWuUpCKAnDf3Jz2D1o= 22 | github.com/miscreant/miscreant.go v0.0.0-20190903041724-6bebe170cbaf/go.mod h1:pBbZyGwC5i16IBkjVKoy/sznA8jPD/K9iedwe1ESE6w= 23 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 24 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 25 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 26 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 27 | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= 28 | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= 29 | github.com/teserakt-io/golang-ed25519 v0.0.0-20200315192543-8255be791ce4 h1:Sq/68UWgBzKT+pLTUTkSf0jS2IUwwXLFlZmeh+nAzQM= 30 | github.com/teserakt-io/golang-ed25519 v0.0.0-20200315192543-8255be791ce4/go.mod h1:9PdLyPiZIiW3UopXyRnPYyjUXSpiQNHRLu8fOsR3o8M= 31 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 32 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= 33 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 34 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 35 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 36 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 37 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 40 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= 42 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 46 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 48 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= 49 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= 50 | -------------------------------------------------------------------------------- /keys/json.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package keys 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | ) 21 | 22 | type keyType int 23 | 24 | // List of keyType for each KeyMaterial 25 | const ( 26 | // symKeyMaterialType defines a keyType for the SymKeyMaterial implementation 27 | symKeyMaterialType keyType = iota 28 | // pubKeyMaterialType defines a keyType for the PubKeyMaterial implementation 29 | pubKeyMaterialType 30 | ) 31 | 32 | // jsonKey defines a wrapper type to json encode a KeyMaterial. 33 | // It's needed to store the actual key type in the marshalled json 34 | // thus allowing to decode the key later to the proper type. 35 | type jsonKey struct { 36 | KeyType keyType `json:"keyType"` 37 | KeyData interface{} `json:"keyData"` 38 | } 39 | 40 | // FromRawJSON allows to unmarshal a json encoded jsonKey from a json RawMessage 41 | // It returns a ready to use KeyMaterial, or an error if it cannot decode it. 42 | func FromRawJSON(raw json.RawMessage) (KeyMaterial, error) { 43 | m := make(map[string]json.RawMessage) 44 | err := json.Unmarshal(raw, &m) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | if _, ok := m["keyType"]; !ok { 50 | return nil, fmt.Errorf("invalid json raw message, expected \"keyType\"") 51 | } 52 | if _, ok := m["keyData"]; !ok { 53 | return nil, fmt.Errorf("invalid json raw message, expected \"keyData\"") 54 | } 55 | 56 | var t keyType 57 | if err := json.Unmarshal(m["keyType"], &t); err != nil { 58 | return nil, err 59 | } 60 | 61 | var clientKey KeyMaterial 62 | switch t { 63 | case symKeyMaterialType: 64 | clientKey = &symKeyMaterial{} 65 | case pubKeyMaterialType: 66 | clientKey = &pubKeyMaterial{} 67 | default: 68 | return nil, fmt.Errorf("unsupported json key type: %v", t) 69 | } 70 | 71 | if err := json.Unmarshal(m["keyData"], clientKey); err != nil { 72 | return nil, err 73 | } 74 | 75 | if err := clientKey.validate(); err != nil { 76 | return nil, fmt.Errorf("invalid clientKey: %v", err) 77 | } 78 | 79 | // Recompute shared key after unmarshalling the keymaterial 80 | // as this field is not exported 81 | pubKey, ok := clientKey.(*pubKeyMaterial) 82 | if !ok { 83 | return clientKey, nil 84 | } 85 | 86 | if err := pubKey.updateSharedKey(); err != nil { 87 | return nil, err 88 | } 89 | 90 | return clientKey, nil 91 | } 92 | -------------------------------------------------------------------------------- /keys/json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package keys 16 | 17 | import ( 18 | "bytes" 19 | "encoding/base64" 20 | "encoding/hex" 21 | "encoding/json" 22 | "fmt" 23 | "testing" 24 | 25 | "golang.org/x/crypto/curve25519" 26 | "golang.org/x/crypto/ed25519" 27 | 28 | e4crypto "github.com/teserakt-io/e4go/crypto" 29 | ) 30 | 31 | var pubKeyJSONTempate = `{ 32 | "keyType": %d, 33 | "keyData":{ 34 | "PrivateKey":"%s", 35 | "SignerID":"%s", 36 | "C2PubKey":%s, 37 | "PubKeys":{ 38 | "%s": "%s" 39 | } 40 | } 41 | }` 42 | 43 | var symKeyJSONTemplate = `{ 44 | "keyType": %d, 45 | "keyData":{ 46 | "Key":"%s" 47 | } 48 | }` 49 | 50 | func TestFromRawJSON(t *testing.T) { 51 | t.Run("FromRawJSON properly decode json ed25519 keys", func(t *testing.T) { 52 | _, privateKey, err := ed25519.GenerateKey(nil) 53 | if err != nil { 54 | t.Fatalf("Failed to generate private key: %v", err) 55 | } 56 | 57 | signerID := e4crypto.HashIDAlias("signerID") 58 | c2PubKey, err := curve25519.X25519(e4crypto.RandomKey(), curve25519.Basepoint) 59 | if err != nil { 60 | t.Fatalf("Failed to generate c2 public key") 61 | } 62 | 63 | c2PubKeyStr, err := json.Marshal(c2PubKey) 64 | if err != nil { 65 | t.Fatalf("Failed to encode c2PubKey to string: %v", err) 66 | } 67 | 68 | pubKeyID := e4crypto.HashIDAlias("pubKeyID1") 69 | pubKeyKey, _, err := ed25519.GenerateKey(nil) 70 | if err != nil { 71 | t.Fatalf("Failed to generate public key: %v", err) 72 | } 73 | 74 | jsonKey := []byte(fmt.Sprintf(pubKeyJSONTempate, 75 | pubKeyMaterialType, 76 | base64.StdEncoding.EncodeToString(privateKey), 77 | base64.StdEncoding.EncodeToString(signerID), 78 | c2PubKeyStr, 79 | hex.EncodeToString(pubKeyID), 80 | base64.StdEncoding.EncodeToString(pubKeyKey), 81 | )) 82 | 83 | k, err := FromRawJSON(jsonKey) 84 | if err != nil { 85 | t.Fatalf("Got error %v, wanted no error when unmarshalling json key", err) 86 | } 87 | 88 | typedKey, ok := k.(*pubKeyMaterial) 89 | if !ok { 90 | t.Fatalf("Wrong key type: got %T, wanted pubKeyMaterial", k) 91 | } 92 | 93 | if !bytes.Equal(typedKey.PrivateKey, privateKey) { 94 | t.Fatalf("Invalid private key: got %v, wanted %v", typedKey.PrivateKey, privateKey) 95 | } 96 | 97 | if !bytes.Equal(typedKey.SignerID, signerID) { 98 | t.Fatalf("Invalid signer ID: got %v, wanted %v", typedKey.SignerID, signerID) 99 | } 100 | 101 | if !bytes.Equal(typedKey.C2PubKey, c2PubKey) { 102 | t.Fatalf("Invalid C2PubKey: got %v, wanted %v", typedKey.C2PubKey, c2PubKey) 103 | } 104 | 105 | if len(typedKey.PubKeys) != 1 { 106 | t.Fatalf("Invalid pubKey count: got %d, wanted 1", len(typedKey.PubKeys)) 107 | } 108 | 109 | pk, ok := typedKey.PubKeys[hex.EncodeToString(pubKeyID)] 110 | if !ok { 111 | t.Fatalf("Expected pubkeys to hold a key for id %s", pubKeyID) 112 | } 113 | 114 | if !bytes.Equal(pk, pubKeyKey) { 115 | t.Fatalf("Invalid pubKey: got %v, wanted %v", pk, pubKeyKey) 116 | } 117 | }) 118 | 119 | t.Run("FromRawJSON properly decode json symmetric keys", func(t *testing.T) { 120 | privateKey := e4crypto.RandomKey() 121 | 122 | jsonKey := []byte(fmt.Sprintf(symKeyJSONTemplate, 123 | symKeyMaterialType, 124 | base64.StdEncoding.EncodeToString(privateKey), 125 | )) 126 | 127 | k, err := FromRawJSON(jsonKey) 128 | if err != nil { 129 | t.Fatalf("Got error %v when unmarshalling json key, wanted no error", err) 130 | } 131 | 132 | typedKey, ok := k.(*symKeyMaterial) 133 | if !ok { 134 | t.Fatalf("Invalid key type: got %T, wanted symKeyMaterial", k) 135 | } 136 | 137 | if !bytes.Equal(typedKey.Key, privateKey) { 138 | t.Fatalf("Invalid private key: got %v, wanted %v", typedKey.Key, privateKey) 139 | } 140 | }) 141 | 142 | t.Run("FromRawJSON properly errors on invalid json input", func(t *testing.T) { 143 | invalidJSONKeys := []string{ 144 | `{}`, 145 | fmt.Sprintf(`{"keyType": %d}`, symKeyMaterialType), 146 | `{"keyData": {}}`, 147 | fmt.Sprintf(`{"keyType": %d, "keyData": {}}`, -1), 148 | `{"keyType": "nope", "keyData": {}}`, 149 | fmt.Sprintf(`{"keyType": %d, "keyData": ""}`, symKeyMaterialType), 150 | "[]", 151 | } 152 | 153 | for _, invalidJSON := range invalidJSONKeys { 154 | _, err := FromRawJSON([]byte(invalidJSON)) 155 | if err == nil { 156 | t.Fatalf("Expected an error when unmarshalling json `%s`", invalidJSON) 157 | } 158 | } 159 | }) 160 | 161 | t.Run("FromRawJSON properly returns error when loading invalid pubkey data", func(t *testing.T) { 162 | validPrivateKey := e4crypto.RandomKey() 163 | validID := e4crypto.HashIDAlias("random") 164 | validCurvePubKey, err := curve25519.X25519(e4crypto.RandomKey(), curve25519.Basepoint) 165 | if err != nil { 166 | t.Fatalf("Failed to generate c2 pubkey: %v", err) 167 | } 168 | validEdPubKey, _, err := ed25519.GenerateKey(nil) 169 | if err != nil { 170 | t.Fatalf("Failed to generate public key: %v", err) 171 | } 172 | 173 | type testData struct { 174 | privateKey []byte 175 | signerID []byte 176 | c2PubKey []byte 177 | pubKeyID []byte 178 | pubKeyKey []byte 179 | } 180 | 181 | testDatas := []testData{ 182 | { 183 | privateKey: validPrivateKey[1:], 184 | signerID: validID, 185 | c2PubKey: validCurvePubKey, 186 | pubKeyID: validID, 187 | pubKeyKey: validEdPubKey, 188 | }, 189 | { 190 | privateKey: validPrivateKey, 191 | signerID: validID[1:], 192 | c2PubKey: validCurvePubKey, 193 | pubKeyID: validID, 194 | pubKeyKey: validEdPubKey, 195 | }, 196 | { 197 | privateKey: validPrivateKey, 198 | signerID: validID, 199 | c2PubKey: validCurvePubKey[1:], 200 | pubKeyID: validID, 201 | pubKeyKey: validEdPubKey, 202 | }, 203 | { 204 | privateKey: validPrivateKey, 205 | signerID: validID, 206 | c2PubKey: validCurvePubKey, 207 | pubKeyID: validID[1:], 208 | pubKeyKey: validEdPubKey, 209 | }, 210 | { 211 | privateKey: validPrivateKey, 212 | signerID: validID, 213 | c2PubKey: validCurvePubKey, 214 | pubKeyID: validID, 215 | pubKeyKey: validEdPubKey[1:], 216 | }, 217 | } 218 | 219 | for _, testData := range testDatas { 220 | c2PubKeyStr, err := json.Marshal(testData.c2PubKey) 221 | if err != nil { 222 | t.Fatalf("Failed to encode c2PubKey to string: %v", err) 223 | } 224 | 225 | jsonKey := []byte(fmt.Sprintf(pubKeyJSONTempate, 226 | pubKeyMaterialType, 227 | base64.StdEncoding.EncodeToString(testData.privateKey), 228 | base64.StdEncoding.EncodeToString(testData.signerID), 229 | c2PubKeyStr, 230 | hex.EncodeToString(testData.pubKeyID), 231 | base64.StdEncoding.EncodeToString(testData.pubKeyKey), 232 | )) 233 | 234 | _, err = FromRawJSON(jsonKey) 235 | if err == nil { 236 | t.Fatalf("An error was expected while unmarshalling invalid pubkey data %#v", testData) 237 | } 238 | } 239 | }) 240 | 241 | t.Run("FromRawJSON properly returns error when loading invalid symkey data", func(t *testing.T) { 242 | jsonKey := []byte(fmt.Sprintf(symKeyJSONTemplate, 243 | symKeyMaterialType, 244 | base64.StdEncoding.EncodeToString(e4crypto.RandomKey()[1:]), 245 | )) 246 | 247 | _, err := FromRawJSON(jsonKey) 248 | if err == nil { 249 | t.Fatal("An error was expected while unmarshalling symkey") 250 | } 251 | }) 252 | } 253 | -------------------------------------------------------------------------------- /keys/publickey.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package keys 16 | 17 | import ( 18 | "encoding/binary" 19 | "encoding/hex" 20 | "encoding/json" 21 | "fmt" 22 | "sync" 23 | "time" 24 | 25 | "golang.org/x/crypto/curve25519" 26 | "golang.org/x/crypto/ed25519" 27 | 28 | e4crypto "github.com/teserakt-io/e4go/crypto" 29 | ) 30 | 31 | // PubKeyMaterial extends the ClientKey and PubKeyStore interfaces for public key implementations 32 | type PubKeyMaterial interface { 33 | KeyMaterial 34 | PubKeyStore 35 | PublicKey() ed25519.PublicKey 36 | } 37 | 38 | // pubKeyMaterial implements PubKeyMaterial to work with public e4 client key 39 | // and PubKeyStore to holds public key needed to verify message signatures 40 | type pubKeyMaterial struct { 41 | PrivateKey ed25519.PrivateKey `json:"privateKey,omitempty"` 42 | SignerID []byte `json:"signerID,omitempty"` 43 | C2PubKey e4crypto.Curve25519PublicKey `json:"c2PubKey,omitempty"` 44 | PubKeys map[string]ed25519.PublicKey `json:"pubKeys,omitempty"` 45 | 46 | mutex sync.RWMutex 47 | sharedKey []byte 48 | } 49 | 50 | var _ PubKeyMaterial = (*pubKeyMaterial)(nil) 51 | var _ json.Marshaler = (*pubKeyMaterial)(nil) 52 | 53 | // NewPubKeyMaterial creates a new KeyMaterial to work with public e4 client key 54 | func NewPubKeyMaterial(signerID []byte, privateKey ed25519.PrivateKey, c2PubKey e4crypto.Curve25519PublicKey) (PubKeyMaterial, error) { 55 | if err := e4crypto.ValidateID(signerID); err != nil { 56 | return nil, fmt.Errorf("invalid signerID: %v", err) 57 | } 58 | 59 | if err := e4crypto.ValidateEd25519PrivKey(privateKey); err != nil { 60 | return nil, fmt.Errorf("invalid private key: %v", err) 61 | } 62 | 63 | if err := e4crypto.ValidateCurve25519PubKey(c2PubKey); err != nil { 64 | return nil, fmt.Errorf("invalid c2 public key: %v", err) 65 | } 66 | 67 | k := &pubKeyMaterial{ 68 | PubKeys: make(map[string]ed25519.PublicKey), 69 | } 70 | 71 | k.C2PubKey = make([]byte, len(c2PubKey)) 72 | copy(k.C2PubKey, c2PubKey) 73 | 74 | k.PrivateKey = make([]byte, len(privateKey)) 75 | copy(k.PrivateKey, privateKey) 76 | 77 | if err := k.updateSharedKey(); err != nil { 78 | return nil, err 79 | } 80 | 81 | k.SignerID = make([]byte, len(signerID)) 82 | copy(k.SignerID, signerID) 83 | 84 | return k, nil 85 | } 86 | 87 | // NewRandomPubKeyMaterial creates a new PubKeyMaterial key from a random ed25519 key 88 | func NewRandomPubKeyMaterial(signerID []byte, c2PubKey e4crypto.Curve25519PublicKey) (PubKeyMaterial, error) { 89 | _, privateKey, err := ed25519.GenerateKey(nil) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return NewPubKeyMaterial(signerID, privateKey, c2PubKey) 95 | } 96 | 97 | // Protect will encrypt and sign the payload with the private key and returns it, or an error if it fail 98 | func (k *pubKeyMaterial) ProtectMessage(payload []byte, topicKey TopicKey) ([]byte, error) { 99 | timestamp := make([]byte, e4crypto.TimestampLen) 100 | binary.LittleEndian.PutUint64(timestamp, uint64(time.Now().Unix())) 101 | 102 | ct, err := e4crypto.Encrypt(topicKey, timestamp, payload) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | protected, err := e4crypto.Sign(k.SignerID, k.PrivateKey, timestamp, ct) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | protectedLen := e4crypto.TimestampLen + e4crypto.IDLen + len(payload) + e4crypto.TagLen + ed25519.SignatureSize 113 | if protectedLen != len(protected) { 114 | return nil, e4crypto.ErrInvalidProtectedLen 115 | } 116 | 117 | return protected, nil 118 | } 119 | 120 | // UnprotectMessage attempts to decrypt the given protected cipher using the given topicKey. 121 | func (k *pubKeyMaterial) UnprotectMessage(protected []byte, topicKey TopicKey) ([]byte, error) { 122 | if len(protected) <= e4crypto.TimestampLen+ed25519.SignatureSize { 123 | return nil, e4crypto.ErrInvalidProtectedLen 124 | } 125 | 126 | // first check timestamp 127 | timestamp := protected[:e4crypto.TimestampLen] 128 | if err := e4crypto.ValidateTimestamp(timestamp); err != nil { 129 | return nil, err 130 | } 131 | 132 | // then check signature 133 | signerID := protected[e4crypto.TimestampLen : e4crypto.TimestampLen+e4crypto.IDLen] 134 | signed := protected[:len(protected)-ed25519.SignatureSize] 135 | sig := protected[len(protected)-ed25519.SignatureSize:] 136 | 137 | pubkey, err := k.GetPubKey(signerID) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | if !ed25519.Verify(ed25519.PublicKey(pubkey), signed, sig) { 143 | return nil, e4crypto.ErrInvalidSignature 144 | } 145 | 146 | ct := protected[e4crypto.TimestampLen+e4crypto.IDLen : len(protected)-ed25519.SignatureSize] 147 | 148 | // finally decrypt 149 | pt, err := e4crypto.Decrypt(topicKey, timestamp, ct) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | return pt, nil 155 | } 156 | 157 | // UnprotectCommand attempt to decrypt a client command from the given protected cipher. 158 | // It will use the material's private key and the c2 public key to create the required symmetric key 159 | func (k *pubKeyMaterial) UnprotectCommand(protected []byte) ([]byte, error) { 160 | if err := e4crypto.ValidateSymKey(k.sharedKey); err != nil { 161 | return nil, fmt.Errorf("invalid shared key: %v", err) 162 | } 163 | 164 | return e4crypto.UnprotectSymKey(protected, k.sharedKey) 165 | } 166 | 167 | // AddPubKey store the given id and key in internal storage 168 | // It is safe for concurrent access 169 | func (k *pubKeyMaterial) AddPubKey(id []byte, pubKey ed25519.PublicKey) error { 170 | k.mutex.Lock() 171 | defer k.mutex.Unlock() 172 | 173 | if err := e4crypto.ValidateEd25519PubKey(pubKey); err != nil { 174 | return err 175 | } 176 | 177 | k.PubKeys[hex.EncodeToString(id)] = pubKey 178 | 179 | return nil 180 | } 181 | 182 | // removePubKey removes the key associated to id on the pubKeyMateriel 183 | // It returns an error if no key can be found with the given id 184 | func (k *pubKeyMaterial) RemovePubKey(id []byte) error { 185 | k.mutex.Lock() 186 | defer k.mutex.Unlock() 187 | 188 | sid := hex.EncodeToString(id) 189 | _, exists := k.PubKeys[sid] 190 | if !exists { 191 | return fmt.Errorf("no public key exists for id: %s", id) 192 | } 193 | 194 | delete(k.PubKeys, sid) 195 | 196 | return nil 197 | } 198 | 199 | // ResetPubKeys removes all public keys stored on the pubKeyMaterial 200 | func (k *pubKeyMaterial) ResetPubKeys() { 201 | k.mutex.Lock() 202 | defer k.mutex.Unlock() 203 | 204 | // The Go compiler in Go1.12 and above recognizes the map clearing idiom 205 | // and makes that very fast, but also it'll alleviate garbage collection pressure. 206 | // so instead of k.PubKeys = make(map[string][]byte), use: 207 | for key := range k.PubKeys { 208 | delete(k.PubKeys, key) 209 | } 210 | } 211 | 212 | // GetPubKeys return a map of stored pubKeys, indexed by their hex encoded ids 213 | func (k *pubKeyMaterial) GetPubKeys() map[string]ed25519.PublicKey { 214 | k.mutex.RLock() 215 | defer k.mutex.RUnlock() 216 | 217 | return k.PubKeys 218 | } 219 | 220 | // GetPubKey return a pubKey associated to given ID, or ErrPubKeyNotFound 221 | // when it doesn't exists 222 | func (k *pubKeyMaterial) GetPubKey(id []byte) (ed25519.PublicKey, error) { 223 | sid := hex.EncodeToString(id) 224 | 225 | key, ok := k.PubKeys[sid] 226 | if !ok { 227 | return nil, ErrPubKeyNotFound 228 | } 229 | 230 | return key, nil 231 | } 232 | 233 | // SetKey will validate the given key and copy it into the pubKeyMaterial key when valid 234 | func (k *pubKeyMaterial) SetKey(key []byte) error { 235 | if err := e4crypto.ValidateEd25519PrivKey(key); err != nil { 236 | return err 237 | } 238 | 239 | sk := make([]byte, len(key)) 240 | copy(sk, key) 241 | 242 | k.PrivateKey = sk 243 | 244 | return k.updateSharedKey() 245 | } 246 | 247 | func (k *pubKeyMaterial) SetC2PubKey(newC2PubKey e4crypto.Curve25519PublicKey) error { 248 | if err := e4crypto.ValidateCurve25519PubKey(newC2PubKey); err != nil { 249 | return err 250 | } 251 | 252 | k.C2PubKey = newC2PubKey 253 | 254 | return k.updateSharedKey() 255 | } 256 | 257 | // MarshalJSON will infer the key type in the marshalled json data 258 | // to be able to know which key to instantiate when unmarshalling back 259 | func (k *pubKeyMaterial) MarshalJSON() ([]byte, error) { 260 | // we have to use a temporary intermediate struct here as 261 | // passing directly k to KeyData would cause an infinite loop of MarshalJSON calls 262 | jsonKey := &jsonKey{ 263 | KeyType: pubKeyMaterialType, 264 | KeyData: struct { 265 | PrivateKey ed25519.PrivateKey 266 | SignerID []byte 267 | C2PubKey []byte 268 | PubKeys map[string]ed25519.PublicKey 269 | }{ 270 | PrivateKey: k.PrivateKey, 271 | SignerID: k.SignerID, 272 | C2PubKey: k.C2PubKey, 273 | PubKeys: k.PubKeys, 274 | }, 275 | } 276 | 277 | return json.Marshal(jsonKey) 278 | } 279 | 280 | // PublicKey returns the public key of the keyMaterial 281 | func (k *pubKeyMaterial) PublicKey() ed25519.PublicKey { 282 | publicPart := k.PrivateKey.Public() 283 | publicKey, ok := publicPart.(ed25519.PublicKey) 284 | if !ok { 285 | panic(fmt.Sprintf("%T is invalid for public key, wanted ed25519.PublicKey", publicPart)) 286 | } 287 | 288 | return publicKey 289 | } 290 | 291 | func (k *pubKeyMaterial) updateSharedKey() error { 292 | curvePrivateKey := e4crypto.PrivateEd25519KeyToCurve25519(k.PrivateKey) 293 | sharedKey, err := curve25519.X25519(curvePrivateKey, k.C2PubKey) 294 | if err != nil { 295 | return fmt.Errorf("curve25519 X25519 failed: %v", err) 296 | } 297 | 298 | k.sharedKey = e4crypto.Sha3Sum256(sharedKey)[:e4crypto.KeyLen] 299 | 300 | return nil 301 | } 302 | 303 | func (k *pubKeyMaterial) validate() error { 304 | if err := e4crypto.ValidateID(k.SignerID); err != nil { 305 | return err 306 | } 307 | if err := e4crypto.ValidateEd25519PrivKey(k.PrivateKey); err != nil { 308 | return err 309 | } 310 | if err := e4crypto.ValidateCurve25519PubKey(k.C2PubKey); err != nil { 311 | return err 312 | } 313 | for id, pubKey := range k.PubKeys { 314 | decodedID, err := hex.DecodeString(id) 315 | if err != nil { 316 | return err 317 | } 318 | 319 | if err := e4crypto.ValidateID(decodedID); err != nil { 320 | return err 321 | } 322 | if err := e4crypto.ValidateEd25519PubKey(pubKey); err != nil { 323 | return err 324 | } 325 | } 326 | 327 | return nil 328 | } 329 | -------------------------------------------------------------------------------- /keys/publickey_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package keys 16 | 17 | import ( 18 | "bytes" 19 | "encoding/binary" 20 | "encoding/json" 21 | "reflect" 22 | "testing" 23 | "time" 24 | 25 | "golang.org/x/crypto/curve25519" 26 | "golang.org/x/crypto/ed25519" 27 | 28 | e4crypto "github.com/teserakt-io/e4go/crypto" 29 | ) 30 | 31 | func TestNewPubKeyMaterial(t *testing.T) { 32 | expectedSignerID := e4crypto.HashIDAlias("test") 33 | _, expectedPrivateKey, err := ed25519.GenerateKey(nil) 34 | if err != nil { 35 | t.Fatalf("Failed to generate ed25519 private key: %v", err) 36 | } 37 | 38 | expectedC2PubKey := getTestC2PubKey(t) 39 | 40 | key, err := NewPubKeyMaterial(expectedSignerID, expectedPrivateKey, expectedC2PubKey) 41 | if err != nil { 42 | t.Fatalf("Key creation failed: %v", err) 43 | } 44 | 45 | assertPubKeyMaterialContains(t, key, expectedSignerID, expectedPrivateKey, expectedC2PubKey) 46 | 47 | invalidSignerID := make([]byte, e4crypto.IDLen-1) 48 | _, err = NewPubKeyMaterial(invalidSignerID, expectedPrivateKey, expectedC2PubKey) 49 | if err == nil { 50 | t.Fatal("Expected an invalid signerID to produce an error when creating a key material") 51 | } 52 | 53 | invalidPrivateKey := make([]byte, len(expectedPrivateKey)) 54 | _, err = NewPubKeyMaterial(expectedSignerID, invalidPrivateKey, expectedC2PubKey) 55 | if err == nil { 56 | t.Fatal("Expected an invalid private key to produce an error when creating a key material") 57 | } 58 | } 59 | 60 | func assertPubKeyMaterialContains( 61 | t *testing.T, 62 | key PubKeyMaterial, 63 | expectedSignerID []byte, 64 | expectedPrivateKey ed25519.PrivateKey, 65 | expectedC2PubKey []byte, 66 | ) { 67 | typedKey, ok := key.(*pubKeyMaterial) 68 | if !ok { 69 | t.Fatalf("Unexpected type: got %T, wanted pubKeyMaterial", key) 70 | } 71 | 72 | if !bytes.Equal(typedKey.SignerID, expectedSignerID) { 73 | t.Fatalf("Invalid signer ID: got %v, wanted %v", typedKey.SignerID, expectedSignerID) 74 | } 75 | 76 | if !bytes.Equal(typedKey.C2PubKey, expectedC2PubKey) { 77 | t.Fatalf("Invalid c2PubKey: got %v, wanted %v", typedKey.C2PubKey, expectedC2PubKey) 78 | } 79 | 80 | if !bytes.Equal(typedKey.PrivateKey, expectedPrivateKey) { 81 | t.Fatalf("Invalid private key: got %v, wanted %v", typedKey.PrivateKey, expectedPrivateKey) 82 | } 83 | } 84 | 85 | func getTestC2PubKey(t *testing.T) []byte { 86 | pubKey, _, err := ed25519.GenerateKey(nil) 87 | if err != nil { 88 | t.Fatalf("Failed to generate ed25519 public key: %v", err) 89 | } 90 | 91 | return pubKey 92 | } 93 | 94 | func TestNewRandomPubKeyMaterial(t *testing.T) { 95 | expectedSignerID := e4crypto.HashIDAlias("test") 96 | expectedC2PubKey := getTestC2PubKey(t) 97 | 98 | key, err := NewRandomPubKeyMaterial(expectedSignerID, expectedC2PubKey) 99 | if err != nil { 100 | t.Fatalf("Failed to create key: %v", err) 101 | } 102 | 103 | typedKey, ok := key.(*pubKeyMaterial) 104 | if !ok { 105 | t.Fatalf("Unexpected type: got %T, wanted pubKeyMaterial", key) 106 | } 107 | 108 | if !bytes.Equal(typedKey.SignerID, expectedSignerID) { 109 | t.Fatalf("Invalid signerID: got %v, wanted: %v", typedKey.SignerID, expectedSignerID) 110 | } 111 | 112 | if !bytes.Equal(typedKey.C2PubKey, expectedC2PubKey) { 113 | t.Fatalf("Invalid c2PubKey: got %v, wanted %v", typedKey.C2PubKey, expectedC2PubKey) 114 | } 115 | 116 | if err := e4crypto.ValidateEd25519PrivKey(typedKey.PrivateKey); err != nil { 117 | t.Fatalf("Failed to validate private key: %v", err) 118 | } 119 | } 120 | 121 | func TestPubKeyMaterialProtectUnprotectMessage(t *testing.T) { 122 | clientID := e4crypto.HashIDAlias("test") 123 | pubKey, privKey, err := ed25519.GenerateKey(nil) 124 | if err != nil { 125 | t.Fatalf("Failed to generate ed25519 keys: %v", err) 126 | } 127 | 128 | k, err := NewPubKeyMaterial(clientID, privKey, getTestC2PubKey(t)) 129 | if err != nil { 130 | t.Fatalf("Failed to create key: %v", err) 131 | } 132 | 133 | payload := []byte("some message") 134 | topicKey := e4crypto.RandomKey() 135 | 136 | protected, err := k.ProtectMessage(payload, topicKey) 137 | if err != nil { 138 | t.Fatalf("Failed to protect message: %v", err) 139 | } 140 | 141 | _, err = k.UnprotectMessage(protected, topicKey) 142 | if err == nil { 143 | t.Fatal("Expected unprotect to fail without the proper public key") 144 | } 145 | 146 | k.AddPubKey(clientID, pubKey) 147 | unprotected, err := k.UnprotectMessage(protected, topicKey) 148 | if err != nil { 149 | t.Fatalf("Failed to unprotect message: %v", err) 150 | } 151 | 152 | if !bytes.Equal(unprotected, payload) { 153 | t.Fatalf("Invalid unprotected message: got %v, wanted: %v", unprotected, payload) 154 | } 155 | 156 | badTopicKey := e4crypto.RandomKey() 157 | _, err = k.UnprotectMessage(protected, badTopicKey) 158 | if err == nil { 159 | t.Fatal("Expected unprotect to fail without the proper topic key") 160 | } 161 | 162 | if _, err := k.UnprotectMessage([]byte("too short"), topicKey); err == nil { 163 | t.Fatal("Expected unprotect to fail with a too short protected message") 164 | } 165 | 166 | if _, err := k.ProtectMessage([]byte("some message"), []byte("not a key")); err == nil { 167 | t.Fatal("Expected protect message to fail with a bad topic key") 168 | } 169 | 170 | tooOldProtected := make([]byte, len(protected)) 171 | copy(tooOldProtected, protected) 172 | 173 | tooOldTs := make([]byte, e4crypto.TimestampLen) 174 | binary.LittleEndian.PutUint64(tooOldTs, uint64(time.Now().Add(-(e4crypto.MaxDelayDuration + 1)).Unix())) 175 | 176 | tooOldProtected = append(tooOldTs, tooOldProtected[e4crypto.TimestampLen:]...) 177 | if _, err := k.UnprotectMessage(tooOldProtected, topicKey); err == nil { 178 | t.Fatal("Expected unprotect message to fail with a too old timestamp") 179 | } 180 | } 181 | 182 | func TestPubKeyMaterialUnprotectCommand(t *testing.T) { 183 | clientID := e4crypto.HashIDAlias("test") 184 | pubKey, privKey, err := ed25519.GenerateKey(nil) 185 | if err != nil { 186 | t.Fatalf("Failed to generate ed25519 keys: %v", err) 187 | } 188 | 189 | c2PrivateCurveKey := e4crypto.RandomKey() 190 | c2PublicCurveKey, err := curve25519.X25519(c2PrivateCurveKey, curve25519.Basepoint) 191 | if err != nil { 192 | t.Fatalf("Failed to generate curve25519 keys: %v", err) 193 | } 194 | 195 | k, err := NewPubKeyMaterial(clientID, privKey, c2PublicCurveKey) 196 | if err != nil { 197 | t.Fatalf("Failed to create key: %v", err) 198 | } 199 | 200 | command := []byte{0x01, 0x02, 0x03, 0x04} 201 | 202 | sharedKey, err := curve25519.X25519(c2PrivateCurveKey, e4crypto.PublicEd25519KeyToCurve25519(pubKey)) 203 | if err != nil { 204 | t.Fatalf("curve25519 X25519 failed: %v", err) 205 | } 206 | 207 | protectedCmd, err := e4crypto.ProtectSymKey(command, e4crypto.Sha3Sum256(sharedKey)) 208 | if err != nil { 209 | t.Fatalf("Failed to protect command: %v", err) 210 | } 211 | 212 | unprotectedCmd, err := k.UnprotectCommand(protectedCmd) 213 | if err != nil { 214 | t.Fatalf("Failed to unprotect command: %v", err) 215 | } 216 | 217 | if !bytes.Equal(unprotectedCmd, command) { 218 | t.Fatalf("Invalid unprotected command: got %v, wanted %v", unprotectedCmd, command) 219 | } 220 | } 221 | 222 | func TestPubKeyMaterialPubKeys(t *testing.T) { 223 | clientID := e4crypto.HashIDAlias("test") 224 | 225 | k, err := NewRandomPubKeyMaterial(clientID, getTestC2PubKey(t)) 226 | if err != nil { 227 | t.Fatalf("Failed to create key: %v", err) 228 | } 229 | 230 | if c := len(k.GetPubKeys()); c != 0 { 231 | t.Fatalf("Invalid pubkey count: got %d, wanted 0", c) 232 | } 233 | 234 | pk0, _, err := ed25519.GenerateKey(nil) 235 | if err != nil { 236 | t.Fatalf("Failed to generate public key: %v", err) 237 | } 238 | if err := k.AddPubKey([]byte("id1"), pk0); err != nil { 239 | t.Fatalf("Failed to add pubkey for id1: %v", err) 240 | } 241 | 242 | pk, err := k.GetPubKey([]byte("id1")) 243 | if err != nil { 244 | t.Fatalf("Failed to get pubKey: %v", err) 245 | } 246 | if !bytes.Equal(pk, pk0) { 247 | t.Fatalf("Invalid pubKey for id1: got %v, wanted %v", pk, pk0) 248 | } 249 | 250 | pk1, _, err := ed25519.GenerateKey(nil) 251 | if err != nil { 252 | t.Fatalf("Failed to generate public key: %v", err) 253 | } 254 | 255 | if err := k.AddPubKey([]byte("id1"), pk1); err != nil { 256 | t.Fatalf("Failed to add pubkey for id1: %v", err) 257 | } 258 | 259 | if c := len(k.GetPubKeys()); c != 1 { 260 | t.Fatalf("Invalid pubkey count: got %d, wanted 1", c) 261 | } 262 | 263 | pk, err = k.GetPubKey([]byte("id1")) 264 | if err != nil { 265 | t.Fatalf("Failed to get pubKey: %v", err) 266 | } 267 | if !bytes.Equal(pk, pk1) { 268 | t.Fatalf("Invalid pubkey for id1: got %v, wanted %v", pk, pk1) 269 | } 270 | 271 | pk2, _, err := ed25519.GenerateKey(nil) 272 | if err != nil { 273 | t.Fatalf("Failed to generate public key: %v", err) 274 | } 275 | 276 | if err := k.AddPubKey([]byte("id2"), pk2); err != nil { 277 | t.Fatalf("Failed to add pubkey for id2: %v", err) 278 | } 279 | 280 | if c := len(k.GetPubKeys()); c != 2 { 281 | t.Fatalf("Invalid pubkey count: got %d, wanted 2", c) 282 | } 283 | 284 | pk, err = k.GetPubKey([]byte("id1")) 285 | if err != nil { 286 | t.Fatalf("Failed to get public key: %v", err) 287 | } 288 | if !bytes.Equal(pk, pk1) { 289 | t.Fatalf("Invalid pubkey for id1: got %v, wanted %v", pk, pk1) 290 | } 291 | 292 | pk, err = k.GetPubKey([]byte("id2")) 293 | if err != nil { 294 | t.Fatalf("Failed to get public key: %v", err) 295 | } 296 | if !bytes.Equal(pk, pk2) { 297 | t.Fatalf("Invalid pubkey for id2: got %v, wanted %v", pk, pk2) 298 | } 299 | 300 | if err := k.RemovePubKey([]byte("id1")); err != nil { 301 | t.Fatalf("Failed to remove pubkey for id1: %v", err) 302 | } 303 | if c := len(k.GetPubKeys()); c != 1 { 304 | t.Fatalf("Invalid pubkey count: got %d, wanted 1", c) 305 | } 306 | 307 | pk, err = k.GetPubKey([]byte("id2")) 308 | if err != nil { 309 | t.Fatalf("Failed to get public key: %v", err) 310 | } 311 | if !bytes.Equal(pk, pk2) { 312 | t.Fatalf("Invalid pubkey for id2: got %v, wanted %v", pk, pk2) 313 | } 314 | 315 | if _, err := k.GetPubKey([]byte("id1")); err != ErrPubKeyNotFound { 316 | t.Fatal("Expected pubkey for id1 to be removed") 317 | } 318 | 319 | // Double remove must return an error 320 | if err := k.RemovePubKey([]byte("id1")); err == nil { 321 | t.Fatal("Expected an error when removing an inexistent pubKey") 322 | } 323 | 324 | // Reset clears all 325 | k.ResetPubKeys() 326 | if c := len(k.GetPubKeys()); c != 0 { 327 | t.Fatalf("Invalid pubkey count: got %d, wanted 0", c) 328 | } 329 | if _, err := k.GetPubKey([]byte("id2")); err != ErrPubKeyNotFound { 330 | t.Fatal("Expected pubkey for id2 to be removed") 331 | } 332 | 333 | // Adding invalid keys return errors 334 | if err := k.AddPubKey([]byte("id1"), []byte("not a key")); err == nil { 335 | t.Fatal("Expected an error when adding an invalid pubKey") 336 | } 337 | } 338 | 339 | func TestPubKeyMaterialSetKey(t *testing.T) { 340 | _, privateKey, err := ed25519.GenerateKey(nil) 341 | if err != nil { 342 | t.Fatalf("Failed to generate key: %v", err) 343 | } 344 | 345 | clientID := e4crypto.HashIDAlias("test") 346 | 347 | k, err := NewPubKeyMaterial(clientID, privateKey, getTestC2PubKey(t)) 348 | if err != nil { 349 | t.Fatalf("Failed to create key: %v", err) 350 | } 351 | 352 | typedKey, ok := k.(*pubKeyMaterial) 353 | if !ok { 354 | t.Fatalf("Unexpected type: got %T, wanted pubKeyMaterial", k) 355 | } 356 | 357 | if !bytes.Equal(typedKey.PrivateKey, privateKey) { 358 | t.Fatalf("Invalid private key: got %v, wanted %v", typedKey.PrivateKey, privateKey) 359 | } 360 | 361 | _, privateKey2, err := ed25519.GenerateKey(nil) 362 | if err != nil { 363 | t.Fatalf("Failed to generate key: %v", err) 364 | } 365 | 366 | if err := typedKey.SetKey(privateKey2); err != nil { 367 | t.Fatalf("Failed to set key: %v", err) 368 | } 369 | 370 | if !bytes.Equal(typedKey.PrivateKey, privateKey2) { 371 | t.Fatalf("Invalid private key: got %v, wanted %v", typedKey.PrivateKey, privateKey2) 372 | } 373 | 374 | if err := typedKey.SetKey([]byte("not a key")); err == nil { 375 | t.Fatal("Expected SetKey with invalid key to returns an error") 376 | } 377 | 378 | privateKey2[0] = privateKey2[0] + 1 379 | if bytes.Equal(typedKey.PrivateKey, privateKey2) { 380 | t.Fatalf("Expected private key slice to have been copied, but it is still pointing to same slice") 381 | } 382 | } 383 | 384 | func TestPubKeyMaterialMarshalJSON(t *testing.T) { 385 | _, privateKey, err := ed25519.GenerateKey(nil) 386 | if err != nil { 387 | t.Fatalf("Failed to generate key: %v", err) 388 | } 389 | 390 | clientID := e4crypto.HashIDAlias("test") 391 | c2Pk := getTestC2PubKey(t) 392 | 393 | k, err := NewPubKeyMaterial(clientID, privateKey, c2Pk) 394 | if err != nil { 395 | t.Fatalf("Failed to create key: %v", err) 396 | } 397 | 398 | pk1, _, err := ed25519.GenerateKey(nil) 399 | if err != nil { 400 | t.Fatalf("Failed to generate public key: %v", err) 401 | } 402 | if err := k.AddPubKey(e4crypto.HashIDAlias("id1"), pk1); err != nil { 403 | t.Fatalf("Failed to add pubkey for id1: %v", err) 404 | } 405 | 406 | pk2, _, err := ed25519.GenerateKey(nil) 407 | if err != nil { 408 | t.Fatalf("Failed to generate public key: %v", err) 409 | } 410 | if err := k.AddPubKey(e4crypto.HashIDAlias("id2"), pk2); err != nil { 411 | t.Fatalf("Failed to add pubkey for id2: %v", err) 412 | } 413 | 414 | jsonKey, err := json.Marshal(k) 415 | if err != nil { 416 | t.Fatalf("Failed to marshal key into json: %v", err) 417 | } 418 | 419 | unmarshalledKey, err := FromRawJSON(jsonKey) 420 | if err != nil { 421 | t.Fatalf("Failed to unmarshal json key: %v", err) 422 | } 423 | 424 | if !reflect.DeepEqual(unmarshalledKey, k) { 425 | t.Fatalf("Invalid unmarshalled key: got %v, wanted %v", unmarshalledKey, k) 426 | } 427 | } 428 | 429 | func TestPrecomputeSharedKey(t *testing.T) { 430 | _, privateKey, err := ed25519.GenerateKey(nil) 431 | if err != nil { 432 | t.Fatalf("Failed to generate key: %v", err) 433 | } 434 | 435 | clientID := e4crypto.HashIDAlias("test") 436 | c2Pk := getTestC2PubKey(t) 437 | 438 | k, err := NewPubKeyMaterial(clientID, privateKey, c2Pk) 439 | if err != nil { 440 | t.Fatalf("Failed to create pubKeyMaterial: %v", err) 441 | } 442 | 443 | typedKey, ok := k.(*pubKeyMaterial) 444 | if !ok { 445 | t.Fatalf("failed to cast key to pubKeyMaterial") 446 | } 447 | 448 | if len(typedKey.sharedKey) == 0 { 449 | t.Fatalf("Expected sharedKey to have length > 0") 450 | } 451 | 452 | if !bytes.Equal(typedKey.C2PubKey, c2Pk) { 453 | t.Fatalf("Expected C2 pubkey to be %v, got %v", c2Pk, typedKey.C2PubKey) 454 | } 455 | 456 | originalSharedKey := make([]byte, len(typedKey.sharedKey)) 457 | copy(originalSharedKey, typedKey.sharedKey) 458 | 459 | newC2Pk := getTestC2PubKey(t) 460 | if err := typedKey.SetC2PubKey(newC2Pk); err != nil { 461 | t.Fatalf("failed to set key: %v", err) 462 | } 463 | 464 | if !bytes.Equal(typedKey.C2PubKey, newC2Pk) { 465 | t.Fatalf("Expected new C2 pubkey to be %v, got %v", newC2Pk, typedKey.C2PubKey) 466 | } 467 | 468 | if bytes.Equal(typedKey.sharedKey, originalSharedKey) { 469 | t.Fatal("Expected shared key to change when c2 key is changed") 470 | } 471 | 472 | copy(originalSharedKey, typedKey.sharedKey) 473 | 474 | _, newPrivateKey, err := ed25519.GenerateKey(nil) 475 | if err != nil { 476 | t.Fatalf("Failed to generate key: %v", err) 477 | } 478 | 479 | if err := typedKey.SetKey(newPrivateKey); err != nil { 480 | t.Fatalf("failed to set key: %v", err) 481 | } 482 | 483 | if !bytes.Equal(typedKey.PrivateKey, newPrivateKey) { 484 | t.Fatalf("Expected new private key to be %v, got %v", newPrivateKey, typedKey.PrivateKey) 485 | } 486 | if bytes.Equal(typedKey.sharedKey, originalSharedKey) { 487 | t.Fatal("Expected shared key to change when private key is changed") 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /keys/symmetric.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package keys 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | 21 | e4crypto "github.com/teserakt-io/e4go/crypto" 22 | ) 23 | 24 | // SymKeyMaterial extends the KeyMaterial interface for symmetric key implementations 25 | type SymKeyMaterial interface { 26 | KeyMaterial 27 | } 28 | 29 | // symKeyMaterial implements SymKeyMaterial 30 | type symKeyMaterial struct { 31 | Key []byte `json:"key,omitempty"` 32 | } 33 | 34 | var _ SymKeyMaterial = (*symKeyMaterial)(nil) 35 | 36 | // NewSymKeyMaterial creates a new SymKeyMaterial 37 | func NewSymKeyMaterial(key []byte) (SymKeyMaterial, error) { 38 | if err := e4crypto.ValidateSymKey(key); err != nil { 39 | return nil, fmt.Errorf("failed to validate sym key: %v", err) 40 | } 41 | 42 | s := &symKeyMaterial{} 43 | 44 | s.Key = make([]byte, len(key)) 45 | copy(s.Key, key) 46 | 47 | return s, nil 48 | } 49 | 50 | // NewRandomSymKeyMaterial creates a new SymKeyMaterial from random value 51 | func NewRandomSymKeyMaterial() (SymKeyMaterial, error) { 52 | return NewSymKeyMaterial(e4crypto.RandomKey()) 53 | } 54 | 55 | // Protect will encrypt payload with the key and returns it, or an error if it fail 56 | func (k *symKeyMaterial) ProtectMessage(payload []byte, topicKey TopicKey) ([]byte, error) { 57 | protected, err := e4crypto.ProtectSymKey(payload, topicKey) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return protected, nil 63 | } 64 | 65 | // UnprotectCommand attempts to decrypt a client command from given protected cipher, 66 | // using the material's key 67 | func (k *symKeyMaterial) UnprotectCommand(protected []byte) ([]byte, error) { 68 | return e4crypto.UnprotectSymKey(protected, k.Key) 69 | } 70 | 71 | // UnprotectMessage attempts to decrypt a message from given protected cipher, 72 | // using given topic key 73 | func (k *symKeyMaterial) UnprotectMessage(protected []byte, topicKey TopicKey) ([]byte, error) { 74 | return e4crypto.UnprotectSymKey(protected, topicKey) 75 | } 76 | 77 | // SetKey will validate the given key and copy it into the SymKeyMaterial private key when valid 78 | func (k *symKeyMaterial) SetKey(key []byte) error { 79 | if err := e4crypto.ValidateSymKey(key); err != nil { 80 | return err 81 | } 82 | 83 | sk := make([]byte, len(key)) 84 | copy(sk, key) 85 | 86 | k.Key = sk 87 | 88 | return nil 89 | } 90 | 91 | // MarshalJSON will infer the key type in the marshalled json data 92 | // to be able to know which key to instantiate when unmarshalling back 93 | func (k *symKeyMaterial) MarshalJSON() ([]byte, error) { 94 | // we have to use a temporary intermediate struct here as 95 | // passing directly k to KeyData would cause an infinite loop of MarshalJSON calls 96 | jsonKey := &jsonKey{ 97 | KeyType: symKeyMaterialType, 98 | KeyData: struct { 99 | Key []byte 100 | }{ 101 | Key: k.Key, 102 | }, 103 | } 104 | 105 | return json.Marshal(jsonKey) 106 | } 107 | 108 | func (k *symKeyMaterial) validate() error { 109 | return e4crypto.ValidateSymKey(k.Key) 110 | } 111 | -------------------------------------------------------------------------------- /keys/symmetric_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package keys 16 | 17 | import ( 18 | "bytes" 19 | "crypto/rand" 20 | "encoding/json" 21 | "reflect" 22 | "testing" 23 | 24 | e4crypto "github.com/teserakt-io/e4go/crypto" 25 | ) 26 | 27 | func TestNewSymKey(t *testing.T) { 28 | t.Run("symKeyMaterial creates key properly", func(t *testing.T) { 29 | expectedKey, err := e4crypto.DeriveSymKey("test password random") 30 | if err != nil { 31 | t.Fatalf("Failed to derive symKeyMaterialMaterial: %v", err) 32 | } 33 | 34 | k, err := NewSymKeyMaterial(expectedKey) 35 | if err != nil { 36 | t.Fatalf("Failed to create symKeyMaterial: %v", err) 37 | } 38 | 39 | tk, ok := k.(*symKeyMaterial) 40 | if !ok { 41 | t.Fatalf("Unexpected type: got %T, wanted symKeyMaterial", k) 42 | } 43 | 44 | if !bytes.Equal(tk.Key, expectedKey) { 45 | t.Fatalf("Invalid key: got %v, wanted %v", tk.Key, expectedKey) 46 | } 47 | }) 48 | 49 | t.Run("creating symKeyMaterial with bad keys returns errors", func(t *testing.T) { 50 | zeroKey := make([]byte, e4crypto.KeyLen) 51 | tooShortKey := make([]byte, e4crypto.KeyLen-1) 52 | tooLongKey := make([]byte, e4crypto.KeyLen+1) 53 | 54 | rand.Read(tooShortKey) 55 | rand.Read(tooLongKey) 56 | 57 | badKeys := [][]byte{ 58 | []byte{}, 59 | zeroKey, 60 | tooShortKey, 61 | tooLongKey, 62 | } 63 | 64 | for _, badKey := range badKeys { 65 | if _, err := NewSymKeyMaterial(badKey); err == nil { 66 | t.Fatalf("Expected an error when trying to create a symKeyMaterial with key %v", badKey) 67 | } 68 | } 69 | }) 70 | } 71 | 72 | func TestNewRandomSymKey(t *testing.T) { 73 | k, err := NewRandomSymKeyMaterial() 74 | if err != nil { 75 | t.Fatalf("Failed to create new random symKeyMaterial: %v", err) 76 | } 77 | 78 | tk, ok := k.(*symKeyMaterial) 79 | if !ok { 80 | t.Fatalf("Unexpected type: got %T, wanted symKeyMaterial", k) 81 | } 82 | 83 | if len(tk.Key) == 0 { 84 | t.Fatal("Expected key to have been set") 85 | } 86 | } 87 | 88 | func TestSymKeyProtectUnprotectMessage(t *testing.T) { 89 | key := e4crypto.RandomKey() 90 | 91 | symKeyMaterial, err := NewSymKeyMaterial(key) 92 | if err != nil { 93 | t.Fatalf("Failed to create symKeyMaterial: %v", err) 94 | } 95 | 96 | topicKey := e4crypto.RandomKey() 97 | expectedMessage := []byte("some test message") 98 | 99 | protected, err := symKeyMaterial.ProtectMessage(expectedMessage, topicKey) 100 | if err != nil { 101 | t.Fatalf("Failed to protect message: %v", err) 102 | } 103 | 104 | unprotected, err := symKeyMaterial.UnprotectMessage(protected, topicKey) 105 | if err != nil { 106 | t.Fatalf("Failed to unprotect message: %v", err) 107 | } 108 | 109 | if !bytes.Equal(unprotected, expectedMessage) { 110 | t.Fatalf("Invalid unprotected message: got %v, wanted %v", unprotected, expectedMessage) 111 | } 112 | 113 | if _, err := symKeyMaterial.ProtectMessage([]byte("message"), []byte("not a key")); err == nil { 114 | t.Fatalf("Expected protectMessage to fail when given an invalid topic key") 115 | } 116 | } 117 | 118 | func TestSymKeyUnprotectCommand(t *testing.T) { 119 | command := []byte{0x01, 0x02, 0x03, 0x04} 120 | key := e4crypto.RandomKey() 121 | 122 | symKeyMaterial, err := NewSymKeyMaterial(key) 123 | if err != nil { 124 | t.Fatalf("Failed to create symKeyMaterial: %v", err) 125 | } 126 | 127 | protectedCommand, err := e4crypto.ProtectSymKey(command, key) 128 | if err != nil { 129 | t.Fatalf("Failed to protect command: %v", err) 130 | } 131 | 132 | unprotectedCommand, err := symKeyMaterial.UnprotectCommand(protectedCommand) 133 | if err != nil { 134 | t.Fatalf("Failed to unprotected command: %v", err) 135 | } 136 | 137 | if !bytes.Equal(unprotectedCommand, command) { 138 | t.Fatalf("Invalid unprotected command: got %v, wanted %v", unprotectedCommand, command) 139 | } 140 | } 141 | 142 | func TestSymKeySetKey(t *testing.T) { 143 | key := e4crypto.RandomKey() 144 | 145 | k, err := NewRandomSymKeyMaterial() 146 | if err != nil { 147 | t.Fatalf("Failed to create symKeyMaterial: %v", err) 148 | } 149 | 150 | tk, ok := k.(*symKeyMaterial) 151 | if !ok { 152 | t.Fatalf("Unexpected type: got %T, wanted symKeyMaterial", k) 153 | } 154 | 155 | if bytes.Equal(tk.Key, key) { 156 | t.Fatalf("Invalid key: got %v, wanted %v", tk.Key, key) 157 | } 158 | 159 | if err := tk.SetKey(key); err != nil { 160 | t.Fatalf("Failed to set key: %v", err) 161 | } 162 | 163 | if !bytes.Equal(tk.Key, key) { 164 | t.Fatalf("Invalid key: got %v, wanted %v", tk.Key, key) 165 | } 166 | 167 | key[0] = key[0] + 1 168 | if bytes.Equal(tk.Key, key) { 169 | t.Fatal("Expected private key slice to have been copied, but it is still pointing to same slice") 170 | } 171 | 172 | if err := tk.SetKey([]byte("not a key")); err == nil { 173 | t.Fatal("Expected setKey to fail with an invalid key") 174 | } 175 | } 176 | 177 | func TestSymKeyMarshalJSON(t *testing.T) { 178 | expectedKey := e4crypto.RandomKey() 179 | k, err := NewSymKeyMaterial(expectedKey) 180 | if err != nil { 181 | t.Fatalf("Failed to generate key: %v", err) 182 | } 183 | 184 | jsonKey, err := json.Marshal(k) 185 | if err != nil { 186 | t.Fatalf("Failed to marshal key to json: %v", err) 187 | } 188 | 189 | unmarshalledKey, err := FromRawJSON(jsonKey) 190 | if err != nil { 191 | t.Fatalf("Failed to unmarshal key from json: %v", err) 192 | } 193 | 194 | if !reflect.DeepEqual(unmarshalledKey, k) { 195 | t.Fatalf("Invalid unmarshalled key: got %v, wanted %#v", unmarshalledKey, k) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /keys/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package keys holds E4 key material implementations. 16 | package keys 17 | 18 | import ( 19 | "errors" 20 | 21 | "golang.org/x/crypto/ed25519" 22 | 23 | e4crypto "github.com/teserakt-io/e4go/crypto" 24 | ) 25 | 26 | var ( 27 | 28 | // ErrPubKeyNotFound occurs when a public key is missing when verifying a signature 29 | ErrPubKeyNotFound = errors.New("signer public key not found") 30 | ) 31 | 32 | // TopicKey defines a custom type for topic keys, avoiding mixing them 33 | // with other keys on the ProtectMessage and UnprotectMessage functions 34 | type TopicKey []byte 35 | 36 | // KeyMaterial defines an interface for E4 client key implementations 37 | // It holds the client private key, and allows to defines how messages will be 38 | // encrypted or decrypted, and how commands will be unprotected. 39 | // A KeyMaterial must also marshal into a jsonKey, allowing the client to properly 40 | // store and load the key material 41 | type KeyMaterial interface { 42 | // ProtectMessage encrypt given payload using the topicKey 43 | // and returns the protected cipher, or an error 44 | ProtectMessage(payload []byte, topicKey TopicKey) ([]byte, error) 45 | // UnprotectMessage decrypt the given cipher using the topicKey 46 | // and returns the clear payload, or an error 47 | UnprotectMessage(protected []byte, topicKey TopicKey) ([]byte, error) 48 | // UnprotectCommand decrypt the given protected command using the key material private key 49 | // and returns the command, or an error 50 | UnprotectCommand(protected []byte) ([]byte, error) 51 | // SetKey sets the material private key, or return an error when the key is invalid 52 | SetKey(key []byte) error 53 | // MarshalJSON marshal the key material into json 54 | MarshalJSON() ([]byte, error) 55 | 56 | // validate performs a keyMaterial validation, 57 | // and returns an error when anything is invalid. 58 | validate() error 59 | } 60 | 61 | // PubKeyStore interface defines methods to interact with a public key storage 62 | // A key material implementing a PubKeyStore enable the client to receive any of the 63 | // pubKey's commands. When the KeyMaterial doesn't implement it, such commands will return 64 | // a ErrUnsupportedOperation error. 65 | type PubKeyStore interface { 66 | // AddPubKey allows to add a public key to the store, identified by ID. 67 | // If a key already exists with this ID, it will be replaced. 68 | AddPubKey(id []byte, key ed25519.PublicKey) error 69 | // GetPubKey returns the public key associated to the ID. 70 | // ErrPubKeyNotFound is returned when it cannot be found. 71 | GetPubKey(id []byte) (ed25519.PublicKey, error) 72 | // GetPubKeys returns all stored public keys, in a ID indexed map. 73 | GetPubKeys() map[string]ed25519.PublicKey 74 | // RemovePubKey removes a public key from the store by its ID, or returns 75 | // an error if it doesn't exists. 76 | RemovePubKey(id []byte) error 77 | // ResetPubKeys removes all public keys stored. 78 | ResetPubKeys() 79 | // SetC2PubKey replaces the current C2 public key with the newly transmitted one. 80 | SetC2PubKey(c2PubKey e4crypto.Curve25519PublicKey) error 81 | } 82 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teserakt-io/e4go/384ed8ac184a0dc588ffa4b08abf0063364e8306/logo.png -------------------------------------------------------------------------------- /scripts/android_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Build Android bindings 3 | # These two environment variable are required: 4 | # export ANDROID_HOME=~/Android/Sdk/ 5 | # export ANDROID_NDK_HOME=~/Android/Sdk/ndk/21.0.6113669/ 6 | # (These are the default paths where Android Studio is installing the SDK and NDK, the version might need to be adjusted depending on your setup) 7 | # A version string can be appended to the output files by specifying a E4VERSION environment variable: 8 | # E4VERSION=v1.1.0 ./scripts/android_bindings.sh 9 | 10 | 11 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 12 | 13 | OUTDIR="${DIR}/../dist/bindings/android" 14 | # List of packages to include in the generated bindings. (ie: keys is not needed) 15 | INCLUDE_GO_PACKAGES="" 16 | 17 | mkdir -p "${OUTDIR}" 2>/dev/null 18 | 19 | gomobile bind -v -target android -o "${OUTDIR}/e4.aar" -javapkg io.teserakt ${DIR}/../ ${DIR}/../crypto 20 | 21 | if [ ! -z "${E4VERSION}" ]; then 22 | mv "${OUTDIR}/e4.aar" "${OUTDIR}/e4_${E4VERSION}.aar" 23 | mv "${OUTDIR}/e4-sources.jar" "${OUTDIR}/e4-sources_${E4VERSION}.jar" 24 | fi 25 | 26 | # gomobile will mess up the go.mod file when running, tidying restore it to the appropriate state 27 | cd "${DIR}/../" && go mod tidy && cd - 28 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go build -o bin/e4keygen ./cmd/e4keygen/e4keygen.go 4 | go build -o bin/e4client ./cmd/e4client/e4client.go 5 | -------------------------------------------------------------------------------- /scripts/devinit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | GIT_VERSION=$(git --version | cut -d" " -f3) 4 | GIT_REQUIRED_VERSION=2.9.0 5 | 6 | if [ "$(printf '%s\n' "$GIT_REQUIRED_VERSION" "$GIT_VERSION" | sort -V | head -n1)" = "$GIT_REQUIRED_VERSION" ]; then 7 | echo "GIT >= 2.9, installing .githooks directory" 8 | git config core.hooksPath .githooks 9 | else 10 | echo "Copying githooks to .git" 11 | #find .git/hooks -type l -exec rm {} \; && find .githooks -type f -exec ln -sf ../../{} .git/hooks/ \; 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/unittest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ -z $(which golint) ]; then 6 | go get golang.org/x/lint/golint 7 | fi 8 | 9 | if [ -z $(which staticcheck) ]; then 10 | go get honnef.co/go/tools/cmd/staticcheck 11 | fi 12 | 13 | echo "Running golint..." 14 | golint -set_exit_status ./... 15 | 16 | echo "Running staticcheck..." 17 | staticcheck ./... 18 | 19 | echo "Running go test..." 20 | # -race increase test time a lot with crypto things, so the timeout must take that into account 21 | go test -timeout 60s -race ./... -coverprofile cover.out 22 | 23 | # Stop here if test have failed, as coverage below will shift 24 | # the test failures up and make it easy to miss. 25 | if [ $? -ne 0 ]; then 26 | echo "FAIL - Some tests have failed." 27 | exit 1 28 | fi 29 | 30 | # coverage report 31 | go tool cover -func cover.out 32 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e4 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io" 21 | ) 22 | 23 | // ReadWriteSeeker is a redefinition of io.ReadWriteSeeker 24 | // to ensure that gomobile bindings still get generated without 25 | // incompatible type removals 26 | type ReadWriteSeeker interface { 27 | io.ReadWriteSeeker 28 | } 29 | 30 | type inMemoryStore struct { 31 | buf []byte 32 | index int 33 | } 34 | 35 | var _ ReadWriteSeeker = (*inMemoryStore)(nil) 36 | 37 | // maxInt holds the maximum int value for the current architecture (32 or 64 bits) 38 | const maxInt = int64(^uint(0) >> 1) 39 | 40 | // NewInMemoryStore creates a new ReadWriteSeeker in memory 41 | func NewInMemoryStore(buf []byte) ReadWriteSeeker { 42 | return &inMemoryStore{ 43 | buf: buf, 44 | } 45 | } 46 | 47 | func (s *inMemoryStore) Write(p []byte) (n int, err error) { 48 | if s.index < len(s.buf) { 49 | s.buf = s.buf[:s.index] 50 | } 51 | 52 | s.buf = append(s.buf, p...) 53 | n = len(p) 54 | s.index += n 55 | 56 | return n, nil 57 | } 58 | 59 | func (s *inMemoryStore) Read(b []byte) (n int, err error) { 60 | if len(b) == 0 { 61 | return 0, nil 62 | } 63 | 64 | if s.index >= len(s.buf) { 65 | return 0, io.EOF 66 | } 67 | 68 | n = copy(b, s.buf[s.index:]) 69 | s.index += n 70 | 71 | return n, nil 72 | } 73 | 74 | // Seek implements io.Seeker. Additionally, an error is returned when offset overflows 75 | // the integer type, according to the plateform bitsize. 76 | func (s *inMemoryStore) Seek(offset int64, whence int) (idx int64, err error) { 77 | if offset > maxInt { 78 | return 0, fmt.Errorf("offset overflow, max int: %d", maxInt) 79 | } 80 | intOffset := int(offset) 81 | 82 | var abs int 83 | switch whence { 84 | case io.SeekStart: 85 | abs = intOffset 86 | case io.SeekCurrent: 87 | abs = s.index + intOffset 88 | case io.SeekEnd: 89 | abs = len(s.buf) + intOffset 90 | default: 91 | return 0, errors.New("invalid whence") 92 | } 93 | 94 | s.index = abs 95 | return int64(abs), nil 96 | } 97 | -------------------------------------------------------------------------------- /storage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Teserakt AG 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package e4 16 | 17 | import ( 18 | "bytes" 19 | "io" 20 | "testing" 21 | ) 22 | 23 | func TestInMemoryStore(t *testing.T) { 24 | store := NewInMemoryStore(nil) 25 | 26 | expected := []byte("abcde") 27 | 28 | n, err := store.Write(expected) 29 | if err != nil { 30 | t.Fatalf("unexpected write error: %v", err) 31 | } 32 | if n != len(expected) { 33 | t.Fatalf("expected n to be %d, got %d", len(expected), n) 34 | } 35 | 36 | readBuf := make([]byte, len(expected)) 37 | _, err = store.Read(readBuf) 38 | if err != io.EOF { 39 | t.Fatalf("expected read EOF, got %v", err) 40 | } 41 | 42 | idx, err := store.Seek(0, io.SeekStart) 43 | if err != nil { 44 | t.Fatalf("unexpected seek error: %v", err) 45 | } 46 | if idx != 0 { 47 | t.Fatalf("unexpected idx, want %d, got %d", 0, idx) 48 | } 49 | 50 | n, err = store.Read(readBuf) 51 | if err != nil { 52 | t.Fatalf("unexpected read error: %v", err) 53 | } 54 | if n != len(expected) { 55 | t.Fatalf("expected n to be %d, got %d", len(expected), n) 56 | } 57 | if !bytes.Equal(readBuf, expected) { 58 | t.Fatalf("expected readBuf to be %v, got %v", expected, readBuf) 59 | } 60 | 61 | idx, err = store.Seek(-2, io.SeekEnd) 62 | if err != nil { 63 | t.Fatalf("unexpected seek error: %v", err) 64 | } 65 | if idx != int64(len(expected)-2) { 66 | t.Fatalf("unexpected idx, want %d, got %d", len(expected)-2, idx) 67 | } 68 | 69 | readBuf = make([]byte, len(expected)-3) 70 | n, err = store.Read(readBuf) 71 | if err != nil { 72 | t.Fatalf("unexpected read error: %v", err) 73 | } 74 | if n != len(expected)-3 { 75 | t.Fatalf("expected n to be %d, got %d", len(expected)-3, n) 76 | } 77 | if !bytes.Equal(readBuf, expected[3:]) { 78 | t.Fatalf("expected readBuf to be %v, got %v", expected[3:], readBuf) 79 | } 80 | 81 | idx, err = store.Seek(-1, io.SeekCurrent) 82 | if err != nil { 83 | t.Fatalf("unexpected seek error: %v", err) 84 | } 85 | if idx != int64(len(expected)-1) { 86 | t.Fatalf("unexpected idx, want %d, got %d", len(expected)-1, idx) 87 | } 88 | 89 | readBuf = make([]byte, 1) 90 | n, err = store.Read(readBuf) 91 | if err != nil { 92 | t.Fatalf("unexpected read error: %v", err) 93 | } 94 | if n != 1 { 95 | t.Fatalf("expected n to be %d, got %d", 1, n) 96 | } 97 | if !bytes.Equal(readBuf, expected[len(expected)-1:]) { 98 | t.Fatalf("expected readBuf to be %v, got %v", expected[len(expected)-1:], readBuf) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teserakt-io/e4go/384ed8ac184a0dc588ffa4b08abf0063364e8306/test/data/.gitkeep --------------------------------------------------------------------------------