├── .github └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── SECURITY.md ├── cmd └── tle │ ├── commands │ ├── commands.go │ ├── commands_test.go │ ├── encrypt.go │ └── flags_test.go │ └── tle.go ├── go.mod ├── go.sum ├── networks ├── fixed │ ├── fixed.go │ └── fixed_test.go └── http │ ├── http.go │ └── http_test.go ├── testdata ├── data.txt ├── decryptedFile.bin ├── encryptedFile.bin ├── lorem-tle-testnet-quicknet-t-2024-01-17-15-28.tle └── lorem.txt ├── tlock.go ├── tlock_age.go ├── tlock_age_test.go └── tlock_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: '1.23' 20 | 21 | - name: Staticheck 22 | run: | 23 | go install honnef.co/go/tools/cmd/staticcheck@latest 24 | staticcheck --version 25 | staticcheck -checks=all ./... 26 | 27 | - name: Build 28 | run: CGO_ENABLED=0 go build -v ./... 29 | 30 | - name: Test 31 | run: CGO_ENABLED=0 go test -short -v ./... 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Setup Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.23' 21 | - run: go version 22 | 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | distribution: goreleaser 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # binaries 2 | cmd/age-plugin-tlock/age-plugin-tlock 3 | cmd/tle/tle 4 | # Mac files 5 | .DS* 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: tlock 2 | 3 | before: 4 | hooks: 5 | - go mod download 6 | 7 | builds: 8 | - id: tlock 9 | binary: tle 10 | main: ./cmd/tle/tle.go 11 | env: 12 | - CGO_ENABLED=0 13 | flags: 14 | - -trimpath 15 | ldflags: 16 | - -s -w -buildid= 17 | goos: 18 | - darwin 19 | - linux 20 | - windows 21 | goarch: 22 | - amd64 23 | - arm 24 | - arm64 25 | goarm: 26 | - 6 27 | - 7 28 | checksum: 29 | name_template: 'checksums.txt' 30 | snapshot: 31 | name_template: "{{ .Tag }}-next" 32 | changelog: 33 | sort: asc 34 | filters: 35 | exclude: 36 | - '^docs:' 37 | - '^test:' 38 | release: 39 | prerelease: auto 40 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 drand team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## tlock: Timelock Encryption/Decryption Made Practical 2 | 3 | tlock gives you time based encryption and decryption capabilities by relying on a [drand](https://drand.love/) threshold network as described in [our tlock paper](https://eprint.iacr.org/2023/189). 4 | It is implemented here as a Go library, which is used to implement the `tle` command line tool enabling anybody to leverage timelock encryption. A compatible Typescript implementation can be found in [tlock-js](https://github.com/drand/tlock-js/) and a third-party implementation in Rust is available at [tlock-rs](https://github.com/thibmeu/tlock-rs). 5 | 6 | Our timelock encryption system relies on an "[unchained drand network](https://drand.love/blog/2022/02/21/multi-frequency-support-and-timelock-encryption-capabilities/)". 7 | 8 | Working endpoints to access it are, on mainnet: 9 | - https://api.drand.sh/ (US) 10 | - https://api2.drand.sh/ (EU) 11 | - https://api3.drand.sh/ (Asia) 12 | - https://drand.cloudflare.com/ (load-balanced across regions) 13 | 14 | On mainnet, the only chainhash supporting timelock encryption, with a 3s frequency and signatures on the G1 group is: 15 | `52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971` 16 | 17 | This is a production-ready network with high-availability guarantees. It is considered fully secure by the drand team 18 | and ran by the same League of Entropy that has been running drand in production since 2019. 19 | 20 | On testnet: 21 | - https://pl-us.testnet.drand.sh/ 22 | - https://pl-eu.testnet.drand.sh/ 23 | - https://testnet0-api.drand.cloudflare.com/ 24 | where we have two networks supporting timelock: 25 | - running with a 3 seconds frequency with signatures on G1: `f3827d772c155f95a9fda8901ddd59591a082df5ac6efe3a479ddb1f5eeb202c` 26 | - running with a 3 seconds frequency with signatures on G2: `7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf` 27 | Note these are relying on the League of Entropy **Testnet**, which should not be considered secure. 28 | 29 | You can also spin up a new drand network and run your own, but note that the security guarantees boil down to the trust you have in your network. 30 | 31 | --- 32 | 33 | ### See How It Works 34 | 35 |

36 | 37 |

38 | 39 | For more details about the scheme and maths, please refer to [our paper on eprint](https://eprint.iacr.org/2023/189). 40 | 41 | --- 42 | 43 | ### Table Of Contents 44 | - [Install the `tle` tool](#install-or build-the-cli) 45 | - [CLI usage](#tle-cli-usage) 46 | - [Timelock Encryption](#timelock-encryption) 47 | - [Timelock Decryption](#timelock-decryption) 48 | - [Library usage](#library-usage) 49 | - [Applying another layer of encryption](#applying-another-layer-of-encryption) 50 | - [Security considerations](#security-considerations) 51 | - [Get in touch](#get-in-touch) 52 | 53 | --- 54 | 55 | ### Install or Build the CLI 56 | 57 | The `tle` tool is pure Go, it works without CGO (`CGO_ENABLED=0`). 58 | To install using Go: 59 | ```bash 60 | go install github.com/drand/tlock/cmd/tle@latest 61 | ``` 62 | 63 | or locally with git: 64 | ```bash 65 | git clone https://github.com/drand/tlock 66 | go build cmd/tle/tle.go 67 | ``` 68 | 69 | **Note:** if you need to decrypt old ciphertexts produced before v1.0.0 against our testnet, you'll need to install the old binary using: 70 | ``` 71 | go install github.com/drand/tlock/cmd/tle@v0.1.0 72 | ``` 73 | We have changed in v1.0.0 the way we are mapping a hash to field element and this is not retro-compatible with the old ciphertext format. 74 | You can still produce ciphertexts against our testnet as explained below using the v1.0.0, but it will use the new hash to field mechanism. 75 | 76 | --- 77 | 78 | ### `tle` CLI Usage 79 | 80 | ``` 81 | Usage: 82 | tle [--encrypt] (-r round)... [--armor] [-o OUTPUT] [INPUT] 83 | tle --decrypt [-o OUTPUT] [INPUT] 84 | 85 | Options: 86 | -e, --encrypt Encrypt the input to the output. Default if omitted. 87 | -d, --decrypt Decrypt the input to the output. 88 | -n, --network The drand API endpoint to use. 89 | -c, --chain The chain to use. Can use either beacon ID name or beacon hash. Use beacon hash in order to ensure public key integrity. 90 | -r, --round The specific round to use to encrypt the message. Cannot be used with --duration. 91 | -f, --force Forces to encrypt against past rounds. 92 | -D, --duration How long to wait before the message can be decrypted. 93 | -o, --output Write the result to the file at path OUTPUT. 94 | -a, --armor Encrypt to a PEM encoded format. 95 | 96 | If the OUTPUT exists, it will be overwritten. 97 | 98 | NETWORK defaults to the drand mainnet endpoint https://api.drand.sh/. 99 | 100 | CHAIN defaults to the chainhash of quicknet: 101 | 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 102 | 103 | You can also use the drand test network: 104 | https://pl-us.testnet.drand.sh/ 105 | and its unchained network on G2 with chainhash 7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf 106 | Note that if you encrypted something prior to March 2023, this was the only available network and used to be the default. 107 | 108 | DURATION, when specified, expects a number followed by one of these units: 109 | "ns", "us" (or "µs"), "ms", "s", "m", "h", "d", "M", "y". 110 | 111 | Example: 112 | $ tle -D 10d -o encrypted_file data_to_encrypt 113 | 114 | After the specified duration: 115 | $ tle -d -o decrypted_file.txt encrypted_file 116 | ``` 117 | 118 | #### Timelock Encryption 119 | 120 | Files can be encrypted using a duration (`--duration/-D`) in which the `encrypted_data` can be decrypted. 121 | 122 | Example using the testnet network and a duration of 5 seconds: 123 | ```bash 124 | $ tle -n="https://pl-us.testnet.drand.sh/" -c="7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf" -D=5s -o=encrypted_data data.txt 125 | ``` 126 | 127 | If a round (`--round/-R`) number is known, it can be used instead of the duration. The data can be decrypted only when that round becomes available in the network. 128 | 129 | Example using an EU relay, the quicknet chainhash and a given round 123456: 130 | ```bash 131 | $ tle -n="https://api2.drand.sh/" -c="52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" -r=123456 -o=encrypted_data data.txt 132 | ``` 133 | 134 | It is also possible to encrypt the data to a PEM encoded format using the armor (`--armor/-a`) flag, 135 | and to rely on the default network and chain hash (which is the `quicknet` one on `api.drand.sh`): 136 | ```bash 137 | $ tle -a -D 20s -o=encrypted_data.PEM data.txt 138 | ``` 139 | 140 | #### Timelock Decryption 141 | 142 | For decryption, it's only necessary to specify the network if you're not using the default one. 143 | Since v1.3.0, some auto-detection of chainhash and network is done upon decryption. 144 | 145 | Using the default values, and printing on stdout: 146 | ```bash 147 | $ tle -d encrypted_data 148 | ``` 149 | 150 | Using the old testnet unchained network and storing the output in a file named "decrypted_data": 151 | ```bash 152 | $ tle -d -n="https://pl-us.testnet.drand.sh/" -c="7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf" 153 | -o=decrypted_data encrypted_data 154 | ``` 155 | Note it will overwrite the `decrypted_data` file if it already exists. 156 | 157 | If decoding an armored source you don't need to specify `-a` again. 158 | 159 | --- 160 | 161 | ### Library Usage 162 | 163 | These example show how to use the API to timelock encrypt and decrypt data. 164 | 165 | #### Timelock Encryption 166 | 167 | ```go 168 | // Open an io.Reader to the data to be encrypted. 169 | in, err := os.Open("data.txt") 170 | if err != nil { 171 | log.Fatalf("open: %s", err) 172 | return 173 | } 174 | defer in.Close() 175 | 176 | // Construct a network that can talk to a drand network. Example using the mainnet quicknet network. 177 | // host: "https://api.drand.sh/" 178 | // chainHash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" 179 | network,err := http.NewNetwork(host, chainHash) 180 | if err != nil { 181 | log.Fatalf("new network: %s", err) 182 | return 183 | } 184 | // Specify how long we need to wait before the file can be decrypted. 185 | duration := 10 * time.Second 186 | 187 | // Use the network to identify the round number that represents the duration. 188 | roundNumber := network.RoundNumber(time.Now().Add(duration)) 189 | 190 | 191 | // Write the encrypted file data to this buffer. 192 | var cipherData bytes.Buffer 193 | 194 | // Encrypt the data for the given round. 195 | if err := tlock.New(network).Encrypt(&cipherData, in, roundNumber); err != nil { 196 | log.Fatalf("encrypt: %v", err) 197 | return 198 | } 199 | ``` 200 | 201 | #### Timelock Decryption 202 | 203 | ```go 204 | // Open an io.Reader to the data to be decrypted. 205 | in, err := os.Open("data.tle") 206 | if err != nil { 207 | log.Fatalf("open: %v", err) 208 | return 209 | } 210 | defer in.Close() 211 | 212 | // Construct a network that can talk to a drand network. 213 | // host: "https://api.drand.sh/" 214 | // chainHash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" 215 | network,err := http.NewNetwork(host, chainHash) 216 | if err != nil { 217 | log.Fatalf("new network: %s", err) 218 | return 219 | } 220 | // Write the decrypted file data to this buffer. 221 | var plainData bytes.Buffer 222 | 223 | // Decrypt the data. If you try to decrypt the data *before* the specified 224 | // duration, it will fail with the message: "too early to decrypt". 225 | if err := tlock.New(network).Decrypt(&plainData, in); err != nil { 226 | log.Fatalf("decrypt: %v", err) 227 | return 228 | } 229 | ``` 230 | 231 | --- 232 | 233 | ### Applying another layer of encryption 234 | 235 | The recommended way of doing "hybrid" encryption where you both encrypt your data using timelock encryption, but also with another encryption scheme, such as a public-key or a symmetric-key scheme is to simple re-encrypt your encrypted data using tlock. 236 | 237 | For example, you can use the [age](https://github.com/FiloSottile/age) cli to encrypt your data with a passphrase as follows. 238 | 239 | #### Encrypting Data With Passphrase 240 | ```bash 241 | $ cat data.txt | age -p | tle -D 30s -o encrypted_data 242 | ``` 243 | 244 | #### Decrypting Data With Passphrase 245 | ```bash 246 | $ cat encrypted_data | tle -d | age -d -o data.txt 247 | ``` 248 | 249 | Note that you could do the same with PGP or any other encryption tool. 250 | 251 | --- 252 | 253 | ### Security considerations 254 | 255 | The security of our timelock encryption mechanism relies on four main things: 256 | - The security of the underlying [Identity Encryption Scheme](https://crypto.stanford.edu/~dabo/pubs/papers/bfibe.pdf) (proposed in 2001) and [its implementation](https://github.com/drand/kyber/blob/a780ab21355ebe7f60b441a586d5e73a40c564eb/encrypt/ibe/ibe.go#L39-L47) that we're using. 257 | - The security of the [threshold BLS scheme](https://link.springer.com/content/pdf/10.1007/s00145-004-0314-9.pdf) (proposed in 2003), and [its impementation](https://github.com/drand/kyber/blob/master/sign/tbls/tbls.go) by the network you're relying on. 258 | - The security of [age](https://age-encryption.org/)'s underlying primitives, and that of the [age implementation](https://age-encryption.org/) we're using to encrypt the data, since we rely on the [hybrid encryption](https://en.wikipedia.org/wiki/Hybrid_cryptosystem) principle, where we only timelock encrypt ("wrap") a random symmetric key that is used by age to actually symmetrically encrypt the data using [Chacha20Poly1305](https://datatracker.ietf.org/doc/html/rfc8439)). 259 | - The security of the threshold network providing you with its BLS signatures **at a given frequency**, for instance the default for `tle` is to rely on drand and its existing League of Entropy network. 260 | 261 | In practice this means that if you trust there are never more than the threshold `t` malicious nodes on the network you're relying on, you are guaranteed that you timelocked data cannot be decrypted earlier than what you intended. 262 | 263 | Please note that neither BLS nor the IBE scheme we are relying on are "quantum resistant", therefore shall a Quantum Computer be built that's able to threaten their security, our current design wouldn't resist. There are also no quantum resistant scheme that we're aware of that could be used to replace our current design since post-quantum signatures schemes do not "thresholdize" too well in a post-quantum IBE-compatible way. 264 | 265 | However, such a quantum computer seems unlikely to be built within the next 5-10 years and therefore we currently consider that you can expect a "**long term security**" horizon of at least 5 years by relying on our design. 266 | 267 | Finally, relying on the League of Entropy **Testnet** should not be considered secure and be used only for testing purposes. We recommend relying on the League of Entropy `fastnet` beacon chain running on **Mainnet** for securing timelocked content. 268 | 269 | Our timelock scheme and code was reviewed by cryptography and security experts from Kudelski and the report is available on IPFS at [`QmWQvTdiD3fSwJgasPLppHZKP6SMvsuTUnb1vRP2xM7y4m`](https://ipfs.io/ipfs/QmWQvTdiD3fSwJgasPLppHZKP6SMvsuTUnb1vRP2xM7y4m). 270 | 271 | --- 272 | 273 | ### Get in touch 274 | 275 | - [Open an issue](https://github.com/drand/tlock/issues/new/choose) for feature requests or to report a bug. 276 | - [Join the drand Slack](https://join.slack.com/t/drandworkspace/shared_invite/zt-19u4rf6if-bf7lxIvF2zYn4~TrBwfkiA) to discuss Timelock, randomness beacons and more. 277 | - Follow the [drand blog](https://drand.love/blog/) for our articles. 278 | - Follow the [@drand_loe](https://twitter.com/drand_loe) account on Twitter to stay tuned. 279 | 280 | --- 281 | 282 | ### License 283 | 284 | This project is licensed using the [Permissive License Stack](https://protocol.ai/blog/announcing-the-permissive-license-stack/) which means that all contributions are available under the most permissive commonly-used licenses, and dependent projects can pick the license that best suits them. 285 | 286 | Therefore, the project is dual-licensed under Apache 2.0 and MIT terms: 287 | 288 | - Apache License, Version 2.0, ([LICENSE-APACHE](https://github.com/drand/drand/blob/master/LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 289 | - MIT license ([LICENSE-MIT](https://github.com/drand/drand/blob/master/LICENSE-MIT) or https://opensource.org/licenses/MIT) 290 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | ## Security Announcements 3 | 4 | [Join the drand slack workspace](https://join.slack.com/t/drandworkspace/shared_invite/zt-19u4rf6if-bf7lxIvF2zYn4~TrBwfkiA) for security and vulnerability announcements. 5 | 6 | ## Reporting a Vulnerability 7 | 8 | If you find any vulnerabilities, please disclose them via email to security@protocol.ai or to a Protocol Labs member on [our slack workspace](https://join.slack.com/t/drandworkspace/shared_invite/zt-19u4rf6if-bf7lxIvF2zYn4~TrBwfkiA). 9 | 10 | Also feel free to timelock encrypt them for a reasonable disclosure period and post the ciphertext publicly to force us to fix them in a timely manner ;) 11 | -------------------------------------------------------------------------------- /cmd/tle/commands/commands.go: -------------------------------------------------------------------------------- 1 | // Package commands implements the processing of the command line flags and 2 | // processing of the encryption operation. 3 | package commands 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/kelseyhightower/envconfig" 12 | ) 13 | 14 | // Default settings. 15 | const ( 16 | // DefaultNetwork is set to the HTTPs relay from drand, you can also use Cloudflare relay or any other relay. 17 | DefaultNetwork = "https://api.drand.sh/" 18 | // DefaultChain is set to the League of Entropy quicknet chainhash. 19 | DefaultChain = "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" 20 | ) 21 | 22 | // ============================================================================= 23 | 24 | const usage = `tlock v1.3.0 -- github.com/drand/tlock 25 | 26 | Usage: 27 | tle [--encrypt] (-r round)... [--armor] [-o OUTPUT] [INPUT] 28 | tle --decrypt [-o OUTPUT] [INPUT] 29 | tle --metadata 30 | 31 | Options: 32 | -m, --metadata Displays the metadata of drand network in yaml format. 33 | -e, --encrypt Encrypt the input to the output. Default if omitted. 34 | -d, --decrypt Decrypt the input to the output. 35 | -n, --network The drand API endpoint to use. 36 | -c, --chain The chain to use. Can use either beacon ID name or beacon hash. Use beacon hash in order to ensure public key integrity. 37 | -r, --round The specific round to use to encrypt the message. Cannot be used with --duration. 38 | -f, --force Forces to encrypt against past rounds. 39 | -D, --duration How long to wait before the message can be decrypted. 40 | -o, --output Write the result to the file at path OUTPUT. 41 | -a, --armor Encrypt to a PEM encoded format. 42 | 43 | If the OUTPUT exists, it will be overwritten. 44 | 45 | NETWORK defaults to the drand mainnet endpoint https://api.drand.sh/. 46 | 47 | CHAIN defaults to the chainhash of quicknet: 48 | 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 49 | 50 | You can also use the drand test network: 51 | https://pl-us.testnet.drand.sh/ 52 | and its unchained network on G2 with chainhash 7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf 53 | Note that if you encrypted something prior to March 2023, this was the only available network and used to be the default. 54 | 55 | DURATION, when specified, expects a number followed by one of these units: 56 | "ns", "us" (or "µs"), "ms", "s", "m", "h", "d", "M", "y". 57 | 58 | Example: 59 | $ tle -D 10d -o encrypted_file data_to_encrypt 60 | 61 | After the specified duration: 62 | $ tle -d -o decrypted_file.txt encrypted_file` 63 | 64 | // PrintUsage displays the usage information. 65 | func PrintUsage(log *log.Logger) { 66 | log.Println(usage) 67 | } 68 | 69 | // ============================================================================= 70 | 71 | // Flags represent the values from the command line. 72 | type Flags struct { 73 | Encrypt bool 74 | Decrypt bool 75 | Force bool 76 | Network string 77 | Chain string 78 | Round uint64 79 | Duration string 80 | Output string 81 | Armor bool 82 | Metadata bool 83 | } 84 | 85 | // Parse will parse the environment variables and command line flags. The command 86 | // line flags will overwrite environment variables. Validation takes place. 87 | func Parse() (Flags, error) { 88 | f := Flags{ 89 | Network: DefaultNetwork, 90 | Chain: DefaultChain, 91 | } 92 | 93 | err := envconfig.Process("tle", &f) 94 | if err != nil { 95 | return f, err 96 | } 97 | parseCmdline(&f) 98 | 99 | if err := validateFlags(&f); err != nil { 100 | return Flags{}, err 101 | } 102 | 103 | return f, nil 104 | } 105 | 106 | // parseCmdline will parse all the command line flags. 107 | // The default value is set to the values parsed by the environment variables. 108 | func parseCmdline(f *Flags) { 109 | flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) } 110 | 111 | flag.BoolVar(&f.Encrypt, "e", f.Encrypt, "encrypt the input to the output") 112 | flag.BoolVar(&f.Encrypt, "encrypt", f.Encrypt, "encrypt the input to the output") 113 | 114 | flag.BoolVar(&f.Decrypt, "d", f.Decrypt, "decrypt the input to the output") 115 | flag.BoolVar(&f.Decrypt, "decrypt", f.Decrypt, "decrypt the input to the output") 116 | 117 | flag.BoolVar(&f.Force, "f", f.Force, "Forces to encrypt against past rounds") 118 | flag.BoolVar(&f.Force, "force", f.Force, "Forces to encrypt against past rounds.") 119 | 120 | flag.StringVar(&f.Network, "n", f.Network, "the drand API endpoint") 121 | flag.StringVar(&f.Network, "network", f.Network, "the drand API endpoint") 122 | 123 | flag.StringVar(&f.Chain, "c", f.Chain, "chain to use") 124 | flag.StringVar(&f.Chain, "chain", f.Chain, "chain to use") 125 | 126 | flag.Uint64Var(&f.Round, "r", f.Round, "the specific round to use; cannot be used with --duration") 127 | flag.Uint64Var(&f.Round, "round", f.Round, "the specific round to use; cannot be used with --duration") 128 | 129 | flag.StringVar(&f.Duration, "D", f.Duration, "how long to wait before being able to decrypt") 130 | flag.StringVar(&f.Duration, "duration", f.Duration, "how long to wait before being able to decrypt") 131 | 132 | flag.StringVar(&f.Output, "o", f.Output, "the path to the output file") 133 | flag.StringVar(&f.Output, "output", f.Output, "the path to the output file") 134 | 135 | flag.BoolVar(&f.Armor, "a", f.Armor, "encrypt to a PEM encoded format") 136 | flag.BoolVar(&f.Armor, "armor", f.Armor, "encrypt to a PEM encoded format") 137 | 138 | flag.BoolVar(&f.Metadata, "m", f.Metadata, "get metadata about the drand network") 139 | flag.BoolVar(&f.Metadata, "metadata", f.Metadata, "get metadata about the drand network") 140 | 141 | flag.Parse() 142 | } 143 | 144 | // validateFlags performs a sanity check of the provided flag information. 145 | func validateFlags(f *Flags) error { 146 | // only one of the three f.Metadata, f.Decrypt or f.Encrypt must be true 147 | count := 0 148 | if f.Metadata { 149 | count++ 150 | } 151 | if f.Encrypt { 152 | count++ 153 | } 154 | if f.Decrypt { 155 | count++ 156 | } 157 | if count != 1 { 158 | return fmt.Errorf("only one of -m/--metadata, -d/--decrypt or -e/--encrypt must be passed") 159 | } 160 | switch { 161 | case f.Metadata: 162 | if f.Chain == "" { 163 | return fmt.Errorf("-c/--chain can't be the empty string") 164 | } 165 | if f.Network == "" { 166 | return fmt.Errorf("-n/--network can't be the empty string") 167 | } 168 | case f.Decrypt: 169 | if f.Duration != "" { 170 | return fmt.Errorf("-D/--duration can't be used with -d/--decrypt") 171 | } 172 | if f.Round != 0 { 173 | return fmt.Errorf("-r/--round can't be used with -d/--decrypt") 174 | } 175 | if f.Armor { 176 | return fmt.Errorf("-a/--armor can't be used with -d/--decrypt") 177 | } 178 | if f.Network != DefaultNetwork { 179 | if f.Chain == DefaultChain { 180 | fmt.Fprintf(os.Stderr, 181 | "You've specified a non-default network endpoint but still use the default chain hash.\n"+ 182 | "You might want to also specify a custom chainhash with the -c/--chain flag.\n\n") 183 | } 184 | } 185 | default: 186 | if f.Chain == "" { 187 | fmt.Fprintf(os.Stderr, "-c/--chain is empty, will default to quicknet chainhash (%s).\n", DefaultChain) 188 | } 189 | if f.Duration != "" && f.Round != 0 { 190 | return fmt.Errorf("-D/--duration can't be used with -r/--round") 191 | } 192 | if f.Duration == "" && f.Round == 0 { 193 | return fmt.Errorf("-D/--duration or -r/--round must be specified") 194 | } 195 | if f.Network != DefaultNetwork { 196 | if f.Chain == DefaultChain { 197 | fmt.Fprintf(os.Stderr, 198 | "You've specified a non-default network endpoint but still use the default chain hash.\n"+ 199 | "You might want to also specify a custom chainhash with the -c/--chain flag.\n\n") 200 | } 201 | } 202 | } 203 | 204 | return nil 205 | } 206 | -------------------------------------------------------------------------------- /cmd/tle/commands/commands_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestParseDuration(t *testing.T) { 13 | type test struct { 14 | name string 15 | duration string 16 | date time.Time 17 | expected time.Duration 18 | err error 19 | } 20 | 21 | tests := []test{ 22 | { 23 | name: "seconds are parsed correctly", 24 | duration: "1s", 25 | date: time.Now(), 26 | expected: 1 * time.Second, 27 | }, 28 | { 29 | name: "hours are parsed correctly", 30 | duration: "1h", 31 | date: time.Now(), 32 | expected: 1 * time.Hour, 33 | }, 34 | { 35 | name: "days are parsed correctly", 36 | duration: "1d", 37 | date: time.Now(), 38 | expected: 24 * time.Hour, 39 | }, 40 | { 41 | name: "months are parsed correctly", 42 | duration: "1M", 43 | date: time.Date(2022, 01, 01, 0, 0, 0, 0, time.UTC), 44 | expected: 31 * 24 * time.Hour, 45 | }, 46 | { 47 | name: "years are parsed correctly", 48 | duration: "1y", 49 | date: time.Date(2022, 01, 01, 0, 0, 0, 0, time.UTC), 50 | expected: 365 * 24 * time.Hour, 51 | }, 52 | { 53 | name: "a mix of timespans parse successfully", 54 | duration: "1y1M1s", 55 | date: time.Date(2022, 01, 01, 0, 0, 0, 0, time.UTC), 56 | expected: 365*24*time.Hour + 31*24*time.Hour + 1*time.Second, 57 | }, 58 | { 59 | name: "a mix of timespans in a funny order parse successfully", 60 | duration: "2s1y1M", 61 | date: time.Date(2022, 01, 01, 0, 0, 0, 0, time.UTC), 62 | expected: 365*24*time.Hour + 31*24*time.Hour + 2*time.Second, 63 | }, 64 | { 65 | name: "times with multiple digits parse successfully", 66 | duration: "203m", 67 | date: time.Date(2022, 01, 01, 0, 0, 0, 0, time.UTC), 68 | expected: 203 * time.Minute, 69 | }, 70 | { 71 | name: "parsing an invalid timespan character fails", 72 | duration: "1C", 73 | date: time.Now(), 74 | err: ErrInvalidDurationFormat, 75 | }, 76 | { 77 | name: "missing multipliers fails", 78 | duration: "DM", 79 | date: time.Now(), 80 | err: ErrInvalidDurationFormat, 81 | }, 82 | { 83 | name: "0 values are in the middle are allowed", 84 | duration: "4y0M1m", 85 | date: time.Now(), 86 | // note that this will fail in 2096-2099 since 2100 is not a leap year 87 | expected: (4*365+1)*24*time.Hour + 1*time.Minute, 88 | }, 89 | { 90 | name: "total of 0 should also be fine", 91 | duration: "0s", 92 | date: time.Now(), 93 | expected: 0 * time.Second, 94 | }, 95 | { 96 | name: "if characters are repeated, an error is returned", 97 | duration: "3s2s1d1s", 98 | date: time.Now(), 99 | err: ErrDuplicateDuration, 100 | }, 101 | } 102 | 103 | for _, tc := range tests { 104 | t.Run(tc.name, func(t *testing.T) { 105 | seconds, err := parseDurationsAsSeconds(tc.date, tc.duration) 106 | if tc.err == nil && err != nil { 107 | t.Fatalf("unexpected parse error: %s", err) 108 | } 109 | 110 | if tc.err != nil && tc.err != err { 111 | t.Fatalf("expecting parsing error '%s'; got %v", ErrInvalidDurationFormat, err) 112 | } 113 | 114 | expected := tc.date.Add(tc.expected) 115 | result := tc.date.Add(seconds) 116 | 117 | if !result.Equal(tc.date.Add(tc.expected)) { 118 | t.Fatalf("expecting end time %s; got %s", expected, result) 119 | } 120 | 121 | }) 122 | } 123 | } 124 | 125 | func TestEncryptionWithDurationOverflow(t *testing.T) { 126 | flags := Flags{ 127 | Encrypt: true, 128 | Decrypt: false, 129 | Network: DefaultNetwork, 130 | Chain: DefaultChain, 131 | Round: 0, 132 | Duration: "292277042628y", 133 | Armor: false, 134 | } 135 | err := Encrypt(flags, os.Stdout, bytes.NewBufferString("very nice"), nil) 136 | require.ErrorIs(t, err, ErrInvalidDurationValue) 137 | } 138 | 139 | func TestEncryptionWithDurationOverflowUsingOtherUnits(t *testing.T) { 140 | flags := Flags{ 141 | Encrypt: true, 142 | Decrypt: false, 143 | Network: DefaultNetwork, 144 | Chain: DefaultChain, 145 | Duration: "292277042627y12m1d", 146 | Armor: false, 147 | } 148 | err := Encrypt(flags, os.Stdout, bytes.NewBufferString("very nice"), nil) 149 | require.ErrorIs(t, err, ErrInvalidDurationValue) 150 | } 151 | -------------------------------------------------------------------------------- /cmd/tle/commands/encrypt.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strconv" 9 | "time" 10 | 11 | "filippo.io/age/armor" 12 | "github.com/drand/tlock" 13 | "github.com/drand/tlock/networks/http" 14 | ) 15 | 16 | var ErrInvalidDurationFormat = errors.New("unsupported duration type or malformed duration - note: drand can only support as short as seconds") 17 | var ErrInvalidDurationValue = errors.New("the duration you entered is either in the past or was too large and would cause an overflow") 18 | 19 | // Encrypt performs the encryption operation. This requires the implementation 20 | // of an encoder for reading/writing to disk, a network for making calls to the 21 | // drand network, and an encrypter for encrypting/decrypting the data. 22 | func Encrypt(flags Flags, dst io.Writer, src io.Reader, network *http.Network) error { 23 | tlock := tlock.New(network) 24 | 25 | if flags.Armor { 26 | a := armor.NewWriter(dst) 27 | defer func() { 28 | if err := a.Close(); err != nil { 29 | fmt.Printf("Error while closing: %v", err) 30 | } 31 | }() 32 | dst = a 33 | } 34 | 35 | switch { 36 | case flags.Round != 0: 37 | lastestAvailableRound := network.RoundNumber(time.Now()) 38 | if !flags.Force && flags.Round < lastestAvailableRound { 39 | return fmt.Errorf("round %d is in the past", flags.Round) 40 | } 41 | 42 | return tlock.Encrypt(dst, src, flags.Round) 43 | 44 | case flags.Duration != "": 45 | start := time.Now() 46 | totalDuration, err := parseDurationsAsSeconds(start, flags.Duration) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | decryptionTime := start.Add(totalDuration) 52 | if decryptionTime.Before(start) || decryptionTime.Equal(start) { 53 | return ErrInvalidDurationValue 54 | } 55 | 56 | roundNumber := network.RoundNumber(decryptionTime) 57 | return tlock.Encrypt(dst, src, roundNumber) 58 | default: 59 | return errors.New("you must provide either duration or a round flag to encrypt") 60 | } 61 | } 62 | 63 | var ErrDuplicateDuration = errors.New("you cannot use the same duration unit specifier twice in one duration") 64 | 65 | func parseDurationsAsSeconds(start time.Time, input string) (time.Duration, error) { 66 | timeUnits := "smhMdwy" 67 | 68 | // first we check that there are no extra characters or malformed groups 69 | valid, err := regexp.Compile(fmt.Sprintf("^([0-9]+[%s]{1})+$", timeUnits)) 70 | if err != nil { 71 | return 0, err 72 | } 73 | if len(valid.FindAll([]byte(input), -1)) != 1 { 74 | return 0, ErrInvalidDurationFormat 75 | } 76 | 77 | // then we iterate through each duration unit and combine them into one 78 | totalDuration := time.Duration(0) 79 | for _, timeUnit := range timeUnits { 80 | r, err := regexp.Compile(fmt.Sprintf("[0-9]+%c", timeUnit)) 81 | if err != nil { 82 | return 0, err 83 | } 84 | matches := r.FindAll([]byte(input), -1) 85 | if len(matches) > 1 { 86 | return 0, ErrDuplicateDuration 87 | } 88 | if len(matches) == 0 { 89 | continue 90 | } 91 | 92 | match := matches[0] 93 | durationLength, err := strconv.Atoi(string(match[0 : len(match)-1])) 94 | if err != nil { 95 | return 0, err 96 | } 97 | 98 | totalDuration += durationFrom(start, durationLength, timeUnit) 99 | } 100 | 101 | return totalDuration, nil 102 | } 103 | 104 | func durationFrom(start time.Time, value int, duration rune) time.Duration { 105 | switch duration { 106 | case 's': 107 | return time.Duration(value) * time.Second 108 | case 'm': 109 | return time.Duration(value) * time.Minute 110 | case 'h': 111 | return time.Duration(value) * time.Hour 112 | case 'd': 113 | return start.AddDate(0, 0, value).Sub(start) 114 | case 'w': 115 | return start.AddDate(0, 0, value*7).Sub(start) 116 | case 'M': 117 | return start.AddDate(0, value, 0).Sub(start) 118 | case 'y': 119 | return start.AddDate(value, 0, 0).Sub(start) 120 | } 121 | return 0 122 | } 123 | -------------------------------------------------------------------------------- /cmd/tle/commands/flags_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type KV struct { 13 | key string 14 | value string 15 | } 16 | 17 | func Test(t *testing.T) { 18 | tests := []struct { 19 | name string 20 | flags []KV 21 | shouldError bool 22 | }{ 23 | { 24 | name: "parsing encrypt fails without duration or round", 25 | flags: []KV{ 26 | { 27 | key: "TLE_ENCRYPT", 28 | value: "true", 29 | }, 30 | }, 31 | shouldError: true, 32 | }, 33 | { 34 | name: "parsing encrypt with both duration and round fails", 35 | flags: []KV{ 36 | { 37 | key: "TLE_ENCRYPT", 38 | value: "true", 39 | }, 40 | { 41 | key: "TLE_DURATION", 42 | value: "1d", 43 | }, 44 | { 45 | key: "TLE_ROUND", 46 | value: "1", 47 | }, 48 | }, 49 | shouldError: true, 50 | }, 51 | { 52 | name: "parsing encrypt with round passes", 53 | flags: []KV{ 54 | { 55 | key: "TLE_ENCRYPT", 56 | value: "true", 57 | }, 58 | { 59 | key: "TLE_ROUND", 60 | value: "1", 61 | }, 62 | }, 63 | shouldError: false, 64 | }, 65 | { 66 | name: "parsing encrypt with just duration passes", 67 | flags: []KV{ 68 | { 69 | key: "TLE_ENCRYPT", 70 | value: "true", 71 | }, 72 | { 73 | key: "TLE_DURATION", 74 | value: "1d", 75 | }, 76 | }, 77 | shouldError: false, 78 | }, 79 | { 80 | name: "parsing encrypt with duration and armor passes", 81 | flags: []KV{ 82 | { 83 | key: "TLE_ENCRYPT", 84 | value: "true", 85 | }, 86 | { 87 | key: "TLE_DURATION", 88 | value: "1d", 89 | }, 90 | { 91 | key: "TLE_ARMOR", 92 | value: "true", 93 | }, 94 | }, 95 | shouldError: false, 96 | }, 97 | { 98 | name: "parsing encrypt fails with decrypt", 99 | flags: []KV{ 100 | { 101 | key: "TLE_ENCRYPT", 102 | value: "true", 103 | }, 104 | { 105 | key: "TLE_DECRYPT", 106 | value: "true", 107 | }, 108 | { 109 | key: "TLE_DURATION", 110 | value: "1d", 111 | }, 112 | }, 113 | shouldError: true, 114 | }, 115 | { 116 | name: "parsing decrypt fails with duration", 117 | flags: []KV{ 118 | { 119 | key: "TLE_DECRYPT", 120 | value: "true", 121 | }, 122 | { 123 | key: "TLE_DURATION", 124 | value: "1d", 125 | }, 126 | }, 127 | shouldError: true, 128 | }, 129 | { 130 | name: "parsing decrypt with round fails", 131 | flags: []KV{ 132 | { 133 | key: "TLE_DECRYPT", 134 | value: "true", 135 | }, 136 | { 137 | key: "TLE_ROUND", 138 | value: "1", 139 | }, 140 | }, 141 | shouldError: true, 142 | }, 143 | { 144 | name: "parsing decrypt with armor fails", 145 | flags: []KV{ 146 | { 147 | key: "TLE_DECRYPT", 148 | value: "true", 149 | }, 150 | { 151 | key: "TLE_ARMOR", 152 | value: "true", 153 | }, 154 | }, 155 | shouldError: true, 156 | }, 157 | { 158 | name: "parsing decrypt alone passes", 159 | flags: []KV{ 160 | { 161 | key: "TLE_DECRYPT", 162 | value: "true", 163 | }, 164 | }, 165 | shouldError: false, 166 | }, 167 | { 168 | name: "passing metadata flag", 169 | flags: []KV{ 170 | { 171 | key: "TLE_METADATA", 172 | value: "true", 173 | }, 174 | }, 175 | shouldError: false, 176 | }, 177 | { 178 | name: "passing metadata flag along with encrypt", 179 | flags: []KV{ 180 | { 181 | key: "TLE_METADATA", 182 | value: "true", 183 | }, 184 | { 185 | key: "TLE_ENCRYPT", 186 | value: "true", 187 | }, 188 | }, 189 | shouldError: true, 190 | }, 191 | { 192 | name: "passing metadata flag along with decrypt", 193 | flags: []KV{ 194 | { 195 | key: "TLE_METADATA", 196 | value: "true", 197 | }, 198 | { 199 | key: "TLE_DECRYPT", 200 | value: "true", 201 | }, 202 | }, 203 | shouldError: true, 204 | }, 205 | { 206 | name: "passing metadata flag along with decrypt and encrypt", 207 | flags: []KV{ 208 | { 209 | key: "TLE_METADATA", 210 | value: "true", 211 | }, 212 | { 213 | key: "TLE_DECRYPT", 214 | value: "true", 215 | }, 216 | { 217 | key: "TLE_ENCRYPT", 218 | value: "true", 219 | }, 220 | }, 221 | shouldError: true, 222 | }, 223 | } 224 | for _, test := range tests { 225 | t.Run(test.name, func(t *testing.T) { 226 | flag.CommandLine = flag.NewFlagSet("testcommandline", flag.ContinueOnError) 227 | r, w, _ := os.Pipe() 228 | defer require.NoError(t, r.Close()) 229 | defer require.NoError(t, w.Close()) 230 | flag.CommandLine.SetOutput(w) 231 | 232 | for _, f := range test.flags { 233 | require.NoError(t, os.Setenv(f.key, f.value)) 234 | t.Cleanup(func() { 235 | require.NoError(t, os.Unsetenv(f.key)) 236 | }) 237 | } 238 | _, err := Parse() 239 | 240 | if test.shouldError { 241 | require.Error(t, err) 242 | } else { 243 | require.NoError(t, err) 244 | out, _ := io.ReadAll(r) 245 | require.NotContains(t, string(out), "flag provided but not defined") 246 | } 247 | }) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /cmd/tle/tle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | 11 | "github.com/drand/tlock" 12 | "github.com/drand/tlock/cmd/tle/commands" 13 | "github.com/drand/tlock/networks/http" 14 | ) 15 | 16 | func main() { 17 | log := log.New(os.Stderr, "", 0) 18 | 19 | if len(os.Args) == 1 { 20 | commands.PrintUsage(log) 21 | return 22 | } 23 | 24 | if err := run(); err != nil { 25 | switch { 26 | case errors.Is(err, tlock.ErrTooEarly): 27 | log.Fatal(errors.Unwrap(err)) 28 | case errors.Is(err, http.ErrNotUnchained): 29 | log.Fatal(http.ErrNotUnchained) 30 | default: 31 | log.Fatal(err) 32 | } 33 | } 34 | } 35 | 36 | func run() error { 37 | var err error 38 | 39 | flags, err := commands.Parse() 40 | if err != nil { 41 | return fmt.Errorf("parse commands: %v", err) 42 | } 43 | 44 | var src io.Reader = os.Stdin 45 | if name := flag.Arg(0); name != "" && name != "-" { 46 | f, err := os.OpenFile(name, os.O_RDONLY, 0600) 47 | if err != nil { 48 | return fmt.Errorf("failed to open input file %q: %v", name, err) 49 | } 50 | defer func(f *os.File) { 51 | err = f.Close() 52 | }(f) 53 | src = f 54 | } 55 | 56 | var dst io.Writer = os.Stdout 57 | if name := flags.Output; name != "" && name != "-" { 58 | f, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0600) 59 | if err != nil { 60 | return fmt.Errorf("failed to open output file %q: %v", name, err) 61 | } 62 | defer func(f *os.File) { 63 | err = f.Close() 64 | }(f) 65 | dst = f 66 | } 67 | 68 | network, err := http.NewNetwork(flags.Network, flags.Chain) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | switch { 74 | case flags.Metadata: 75 | err = tlock.New(network).Metadata(dst) 76 | case flags.Decrypt: 77 | err = tlock.New(network).Decrypt(dst, src) 78 | default: 79 | err = commands.Encrypt(flags, dst, src, network) 80 | } 81 | 82 | return err 83 | } 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/drand/tlock 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | filippo.io/age v1.2.1 9 | github.com/drand/drand/v2 v2.1.2 10 | github.com/drand/go-clients v0.2.3 11 | github.com/drand/kyber v1.3.1 12 | github.com/drand/kyber-bls12381 v0.3.3 13 | github.com/stretchr/testify v1.10.0 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 19 | github.com/klauspost/compress v1.18.0 // indirect 20 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 21 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 22 | go.dedis.ch/fixbuf v1.0.3 // indirect 23 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect 24 | ) 25 | 26 | require ( 27 | github.com/BurntSushi/toml v1.5.0 // indirect 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 | github.com/hashicorp/errwrap v1.1.0 // indirect 31 | github.com/hashicorp/go-multierror v1.1.1 // indirect 32 | github.com/hashicorp/golang-lru v1.0.2 // indirect 33 | github.com/kelseyhightower/envconfig v1.4.0 34 | github.com/kilic/bls12-381 v0.1.0 // indirect 35 | github.com/nikkolasg/hexjson v0.1.0 // indirect 36 | github.com/prometheus/client_golang v1.22.0 // indirect 37 | github.com/prometheus/client_model v0.6.2 // indirect 38 | github.com/prometheus/common v0.63.0 // indirect 39 | github.com/prometheus/procfs v0.16.1 // indirect 40 | go.uber.org/multierr v1.11.0 // indirect 41 | go.uber.org/zap v1.27.0 // indirect 42 | golang.org/x/crypto v0.38.0 // indirect 43 | golang.org/x/net v0.40.0 // indirect 44 | golang.org/x/sys v0.33.0 // indirect 45 | golang.org/x/text v0.25.0 // indirect 46 | google.golang.org/grpc v1.72.0 // indirect 47 | google.golang.org/protobuf v1.36.6 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= 2 | c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= 3 | filippo.io/age v1.2.0 h1:vRDp7pUMaAJzXNIWJVAZnEf/Dyi4Vu4wI8S1LBzufhE= 4 | filippo.io/age v1.2.0/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= 5 | filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= 6 | filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= 7 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 8 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 9 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 10 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 11 | github.com/ardanlabs/darwin/v2 v2.0.0 h1:XCisQMgQ5EG+ZvSEcADEo+pyfIMKyWAGnn5o2TgriYE= 12 | github.com/ardanlabs/darwin/v2 v2.0.0/go.mod h1:MubZ2e9DAYGaym0mClSOi183NYahrrfKxvSy1HMhoes= 13 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= 16 | github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 17 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 18 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 19 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 20 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 21 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 22 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 23 | github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= 24 | github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= 25 | github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= 26 | github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/drand/drand/v2 v2.0.4 h1:aHV+WAisUaIz/m/xyxRtKGz1WJ5K2Ta1L0csA/ICDRU= 30 | github.com/drand/drand/v2 v2.0.4/go.mod h1:nWBj4w7TA3R8xCoyLzkmsESjTlg4QgNSFAiRR9qZXt8= 31 | github.com/drand/drand/v2 v2.1.2 h1:Q5Fo21BB5PbWQFK0nEB7ldfWlQGxC0qJmvdxjhHGrMY= 32 | github.com/drand/drand/v2 v2.1.2/go.mod h1:8TT+5oKwd+A3dJCFjE/qE0PC8VaF6xcgMpU3TxbRybg= 33 | github.com/drand/go-clients v0.2.1 h1:A2MpNKKrkRue4W/RIWoERjWwImvSpX0im+CqES7oF/c= 34 | github.com/drand/go-clients v0.2.1/go.mod h1:4m2qC/O8lx2Aj6DEIrEZ4kUzAUV6BIjmiSouW6lpYfI= 35 | github.com/drand/go-clients v0.2.3 h1:VEIkoW/S4bOkjrwTUjHlRKnlT08WlobnMVI+0NlYVww= 36 | github.com/drand/go-clients v0.2.3/go.mod h1:4Vo4r+ASfbxfYfTKhT0BMkhqaJxYoWbTJjo8u7Znqkw= 37 | github.com/drand/kyber v1.3.1 h1:E0p6M3II+loMVwTlAp5zu4+GGZFNiRfq02qZxzw2T+Y= 38 | github.com/drand/kyber v1.3.1/go.mod h1:f+mNHjiGT++CuueBrpeMhFNdKZAsy0tu03bKq9D5LPA= 39 | github.com/drand/kyber-bls12381 v0.3.1 h1:KWb8l/zYTP5yrvKTgvhOrk2eNPscbMiUOIeWBnmUxGo= 40 | github.com/drand/kyber-bls12381 v0.3.1/go.mod h1:H4y9bLPu7KZA/1efDg+jtJ7emKx+ro3PU7/jWUVt140= 41 | github.com/drand/kyber-bls12381 v0.3.3 h1:sLl0ILJtB4+POHAKq6tdnWyg+iXADE0LjVKN91RI8JI= 42 | github.com/drand/kyber-bls12381 v0.3.3/go.mod h1:uVRWtcZDAApOWFMwoJVcTfC4csVxXmpkdoSCUZJ5QOY= 43 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 44 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 45 | github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= 46 | github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 47 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 48 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 49 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 50 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 51 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 52 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 53 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 54 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 55 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= 56 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= 57 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 58 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 59 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= 60 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 61 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 62 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 63 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 64 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 65 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 66 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 67 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 68 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 69 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 70 | github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= 71 | github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= 72 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 73 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 74 | github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= 75 | github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig= 76 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 77 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 78 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 79 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 80 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 81 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 82 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 83 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 84 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 85 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 86 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 87 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 88 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 89 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 90 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 91 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 92 | github.com/nikkolasg/hexjson v0.1.0 h1:Cgi1MSZVQFoJKYeRpBNEcdF3LB+Zo4fYKsDz7h8uJYQ= 93 | github.com/nikkolasg/hexjson v0.1.0/go.mod h1:fbGbWFZ0FmJMFbpCMtJpwb0tudVxSSZ+Es2TsCg57cA= 94 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 95 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 96 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 97 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 98 | github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= 99 | github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 100 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 101 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 102 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 103 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 104 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 105 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 106 | github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= 107 | github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= 108 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 109 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 110 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 111 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 112 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 113 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 114 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 115 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 116 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 117 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 118 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 119 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 120 | go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= 121 | go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= 122 | go.dedis.ch/protobuf v1.0.11 h1:FTYVIEzY/bfl37lu3pR4lIj+F9Vp1jE8oh91VmxKgLo= 123 | go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= 124 | go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= 125 | go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= 126 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= 127 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= 128 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= 129 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= 130 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 131 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 132 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= 133 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= 134 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= 135 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= 136 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 137 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 138 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 139 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 140 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 141 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 142 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 143 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 144 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 145 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 146 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 147 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 148 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 149 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 150 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 151 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 152 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 153 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 154 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 155 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 156 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 157 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 158 | golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 159 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 160 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 161 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 162 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 163 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 164 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 165 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 166 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 167 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= 168 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= 169 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= 170 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= 171 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM= 172 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 173 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= 174 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 175 | google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= 176 | google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 177 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 178 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 179 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 180 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 181 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 182 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 183 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 184 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 185 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 186 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 187 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= 188 | -------------------------------------------------------------------------------- /networks/fixed/fixed.go: -------------------------------------------------------------------------------- 1 | // Package fixed implements the Network interface for the tlock package without actual networking. 2 | package fixed 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "time" 8 | 9 | chain "github.com/drand/drand/v2/common" 10 | "github.com/drand/drand/v2/crypto" 11 | 12 | "github.com/drand/kyber" 13 | ) 14 | 15 | // Network represents the network support using the drand http client. 16 | type Network struct { 17 | chainHash string 18 | publicKey kyber.Point 19 | scheme *crypto.Scheme 20 | period time.Duration 21 | genesis int64 22 | fixedSig []byte 23 | } 24 | 25 | // ErrNotUnchained represents an error when the informed chain belongs to a 26 | // chained network. 27 | var ErrNotUnchained = errors.New("not an unchained network") 28 | 29 | // NewNetwork constructs a network with static, fixed data 30 | func NewNetwork(chainHash string, publicKey kyber.Point, sch *crypto.Scheme, period time.Duration, genesis int64, sig []byte) (*Network, error) { 31 | switch sch.Name { 32 | case crypto.ShortSigSchemeID: 33 | case crypto.SigsOnG1ID: 34 | case crypto.UnchainedSchemeID: 35 | case crypto.BN254UnchainedOnG1SchemeID: 36 | default: 37 | return nil, ErrNotUnchained 38 | } 39 | 40 | return &Network{ 41 | chainHash: chainHash, 42 | publicKey: publicKey, 43 | scheme: sch, 44 | period: period, 45 | genesis: genesis, 46 | fixedSig: sig, 47 | }, nil 48 | } 49 | 50 | type infoV2 struct { 51 | PublicKey chain.HexBytes `json:"public_key"` 52 | ID string `json:"beacon_id"` 53 | Period int64 `json:"period"` 54 | Scheme string `json:"scheme"` 55 | GenesisTime int64 `json:"genesis_time"` 56 | ChainHash string `json:"chain_hash"` 57 | } 58 | 59 | func FromInfo(jsonInfo string) (*Network, error) { 60 | info := new(infoV2) 61 | err := json.Unmarshal([]byte(jsonInfo), info) 62 | if err != nil { 63 | return nil, err 64 | } 65 | sch, err := crypto.SchemeFromName(info.Scheme) 66 | if err != nil { 67 | return nil, err 68 | } 69 | public := sch.KeyGroup.Point() 70 | if err := public.UnmarshalBinary(info.PublicKey); err != nil { 71 | return nil, err 72 | } 73 | return NewNetwork(info.ChainHash, public, sch, time.Duration(info.Period)*time.Second, info.GenesisTime, nil) 74 | } 75 | 76 | func (n *Network) SetSignature(sig []byte) { 77 | n.fixedSig = sig 78 | } 79 | 80 | // ChainHash returns the chain hash for this network. 81 | func (n *Network) ChainHash() string { 82 | return n.chainHash 83 | } 84 | 85 | // Current returns the current round for that network at the given date. 86 | func (n *Network) Current(date time.Time) uint64 { 87 | return chain.CurrentRound(date.Unix(), n.period, n.genesis) 88 | } 89 | 90 | // PublicKey returns the kyber point needed for encryption and decryption. 91 | func (n *Network) PublicKey() kyber.Point { 92 | return n.publicKey 93 | } 94 | 95 | // Scheme returns the drand crypto Scheme used by the network. 96 | func (n *Network) Scheme() crypto.Scheme { 97 | return *n.scheme 98 | } 99 | 100 | // Signature only returns a fixed signature if set with the fixed network 101 | func (n *Network) Signature(_ uint64) ([]byte, error) { 102 | return n.fixedSig, nil 103 | } 104 | 105 | // RoundNumber will return the latest round of randomness that is available 106 | func (n *Network) RoundNumber(t time.Time) uint64 { 107 | // + 1 because round 1 happened at genesis time 108 | // integer division makes sure it ticks only every period 109 | return uint64(((t.Unix() - n.genesis) / int64(n.period.Seconds())) + 1) 110 | } 111 | 112 | // SwitchChainHash allows to start using another chainhash on the same host network 113 | func (n *Network) SwitchChainHash(c string) error { 114 | n.chainHash = c 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /networks/fixed/fixed_test.go: -------------------------------------------------------------------------------- 1 | package fixed 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/drand/drand/v2/crypto" 9 | "github.com/drand/kyber" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestFromInfo(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | jsonStr string 17 | wantHash string 18 | wantScheme string 19 | wantErr error 20 | }{ 21 | { 22 | name: "default", 23 | jsonStr: `{"public_key":"868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31","period":30,"genesis_time":1595431050,"genesis_seed":"176f93498eac9ca337150b46d21dd58673ea4e3581185f869672e59fa4cb390a","chain_hash":"8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce","scheme":"pedersen-bls-chained","beacon_id":"default"}`, 24 | wantHash: "", 25 | wantScheme: "", 26 | wantErr: ErrNotUnchained, 27 | }, { 28 | name: "evmnet", 29 | jsonStr: `{"public_key":"07e1d1d335df83fa98462005690372c643340060d205306a9aa8106b6bd0b3820557ec32c2ad488e4d4f6008f89a346f18492092ccc0d594610de2732c8b808f0095685ae3a85ba243747b1b2f426049010f6b73a0cf1d389351d5aaaa1047f6297d3a4f9749b33eb2d904c9d9ebf17224150ddd7abd7567a9bec6c74480ee0b","period":3,"genesis_time":1727521075,"genesis_seed":"cd7ad2f0e0cce5d8c288f2dd016ffe7bc8dc88dbb229b3da2b6ad736490dfed6","chain_hash":"04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3","scheme":"bls-bn254-unchained-on-g1","beacon_id":"evmnet"}`, 30 | wantHash: "04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3", 31 | wantScheme: "bls-bn254-unchained-on-g1", 32 | wantErr: nil, 33 | }, { 34 | name: "quicknet", 35 | jsonStr: `{"public_key":"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a","period":3,"genesis_time":1692803367,"genesis_seed":"f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e","chain_hash":"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971","scheme":"bls-unchained-g1-rfc9380","beacon_id":"quicknet"}`, 36 | wantHash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", 37 | wantScheme: "bls-unchained-g1-rfc9380", 38 | wantErr: nil, 39 | }, 40 | } 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | got, err := FromInfo(tt.jsonStr) 44 | require.ErrorIs(t, err, tt.wantErr) 45 | if err == nil { 46 | if got.ChainHash() != tt.wantHash { 47 | t.Errorf("FromInfo() got = %v, want %v", got.ChainHash(), tt.wantHash) 48 | } 49 | if got.Scheme().Name != tt.wantScheme { 50 | t.Errorf("FromInfo() got = %v, want %v", got.ChainHash(), tt.wantHash) 51 | } 52 | require.Equal(t, uint64(1), got.RoundNumber(time.Unix(got.genesis, 0))) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func TestNetwork_ChainHash(t *testing.T) { 59 | type fields struct { 60 | chainHash string 61 | publicKey kyber.Point 62 | scheme *crypto.Scheme 63 | period time.Duration 64 | genesis int64 65 | fixedSig []byte 66 | } 67 | tests := []struct { 68 | name string 69 | fields fields 70 | want string 71 | }{ 72 | // TODO: Add test cases. 73 | } 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | n := &Network{ 77 | chainHash: tt.fields.chainHash, 78 | publicKey: tt.fields.publicKey, 79 | scheme: tt.fields.scheme, 80 | period: tt.fields.period, 81 | genesis: tt.fields.genesis, 82 | fixedSig: tt.fields.fixedSig, 83 | } 84 | if got := n.ChainHash(); got != tt.want { 85 | t.Errorf("ChainHash() = %v, want %v", got, tt.want) 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func TestNetwork_Current(t *testing.T) { 92 | type fields struct { 93 | chainHash string 94 | publicKey kyber.Point 95 | scheme *crypto.Scheme 96 | period time.Duration 97 | genesis int64 98 | fixedSig []byte 99 | } 100 | type args struct { 101 | date time.Time 102 | } 103 | tests := []struct { 104 | name string 105 | fields fields 106 | args args 107 | want uint64 108 | }{ 109 | // TODO: Add test cases. 110 | } 111 | for _, tt := range tests { 112 | t.Run(tt.name, func(t *testing.T) { 113 | n := &Network{ 114 | chainHash: tt.fields.chainHash, 115 | publicKey: tt.fields.publicKey, 116 | scheme: tt.fields.scheme, 117 | period: tt.fields.period, 118 | genesis: tt.fields.genesis, 119 | fixedSig: tt.fields.fixedSig, 120 | } 121 | if got := n.Current(tt.args.date); got != tt.want { 122 | t.Errorf("Current() = %v, want %v", got, tt.want) 123 | } 124 | }) 125 | } 126 | } 127 | 128 | func TestNetwork_PublicKey(t *testing.T) { 129 | type fields struct { 130 | chainHash string 131 | publicKey kyber.Point 132 | scheme *crypto.Scheme 133 | period time.Duration 134 | genesis int64 135 | fixedSig []byte 136 | } 137 | tests := []struct { 138 | name string 139 | fields fields 140 | want kyber.Point 141 | }{ 142 | // TODO: Add test cases. 143 | } 144 | for _, tt := range tests { 145 | t.Run(tt.name, func(t *testing.T) { 146 | n := &Network{ 147 | chainHash: tt.fields.chainHash, 148 | publicKey: tt.fields.publicKey, 149 | scheme: tt.fields.scheme, 150 | period: tt.fields.period, 151 | genesis: tt.fields.genesis, 152 | fixedSig: tt.fields.fixedSig, 153 | } 154 | if got := n.PublicKey(); !reflect.DeepEqual(got, tt.want) { 155 | t.Errorf("PublicKey() = %v, want %v", got, tt.want) 156 | } 157 | }) 158 | } 159 | } 160 | 161 | func TestNetwork_RoundNumber(t *testing.T) { 162 | type fields struct { 163 | chainHash string 164 | publicKey kyber.Point 165 | scheme *crypto.Scheme 166 | period time.Duration 167 | genesis int64 168 | fixedSig []byte 169 | } 170 | type args struct { 171 | t time.Time 172 | } 173 | tests := []struct { 174 | name string 175 | fields fields 176 | args args 177 | want uint64 178 | }{ 179 | // TODO: Add test cases. 180 | } 181 | for _, tt := range tests { 182 | t.Run(tt.name, func(t *testing.T) { 183 | n := &Network{ 184 | chainHash: tt.fields.chainHash, 185 | publicKey: tt.fields.publicKey, 186 | scheme: tt.fields.scheme, 187 | period: tt.fields.period, 188 | genesis: tt.fields.genesis, 189 | fixedSig: tt.fields.fixedSig, 190 | } 191 | if got := n.RoundNumber(tt.args.t); got != tt.want { 192 | t.Errorf("RoundNumber() = %v, want %v", got, tt.want) 193 | } 194 | }) 195 | } 196 | } 197 | 198 | func TestNetwork_Scheme(t *testing.T) { 199 | type fields struct { 200 | chainHash string 201 | publicKey kyber.Point 202 | scheme *crypto.Scheme 203 | period time.Duration 204 | genesis int64 205 | fixedSig []byte 206 | } 207 | tests := []struct { 208 | name string 209 | fields fields 210 | want crypto.Scheme 211 | }{ 212 | // TODO: Add test cases. 213 | } 214 | for _, tt := range tests { 215 | t.Run(tt.name, func(t *testing.T) { 216 | n := &Network{ 217 | chainHash: tt.fields.chainHash, 218 | publicKey: tt.fields.publicKey, 219 | scheme: tt.fields.scheme, 220 | period: tt.fields.period, 221 | genesis: tt.fields.genesis, 222 | fixedSig: tt.fields.fixedSig, 223 | } 224 | if got := n.Scheme(); !reflect.DeepEqual(got, tt.want) { 225 | t.Errorf("Scheme() = %v, want %v", got, tt.want) 226 | } 227 | }) 228 | } 229 | } 230 | 231 | func TestNetwork_SetSignature(t *testing.T) { 232 | type fields struct { 233 | chainHash string 234 | publicKey kyber.Point 235 | scheme *crypto.Scheme 236 | period time.Duration 237 | genesis int64 238 | fixedSig []byte 239 | } 240 | type args struct { 241 | sig []byte 242 | } 243 | tests := []struct { 244 | name string 245 | fields fields 246 | args args 247 | }{ 248 | // TODO: Add test cases. 249 | } 250 | for _, tt := range tests { 251 | t.Run(tt.name, func(t *testing.T) { 252 | n := &Network{ 253 | chainHash: tt.fields.chainHash, 254 | publicKey: tt.fields.publicKey, 255 | scheme: tt.fields.scheme, 256 | period: tt.fields.period, 257 | genesis: tt.fields.genesis, 258 | fixedSig: tt.fields.fixedSig, 259 | } 260 | n.SetSignature(tt.args.sig) 261 | }) 262 | } 263 | } 264 | 265 | func TestNetwork_Signature(t *testing.T) { 266 | type fields struct { 267 | chainHash string 268 | publicKey kyber.Point 269 | scheme *crypto.Scheme 270 | period time.Duration 271 | genesis int64 272 | fixedSig []byte 273 | } 274 | type args struct { 275 | in0 uint64 276 | } 277 | tests := []struct { 278 | name string 279 | fields fields 280 | args args 281 | want []byte 282 | wantErr bool 283 | }{ 284 | // TODO: Add test cases. 285 | } 286 | for _, tt := range tests { 287 | t.Run(tt.name, func(t *testing.T) { 288 | n := &Network{ 289 | chainHash: tt.fields.chainHash, 290 | publicKey: tt.fields.publicKey, 291 | scheme: tt.fields.scheme, 292 | period: tt.fields.period, 293 | genesis: tt.fields.genesis, 294 | fixedSig: tt.fields.fixedSig, 295 | } 296 | got, err := n.Signature(tt.args.in0) 297 | if (err != nil) != tt.wantErr { 298 | t.Errorf("Signature() error = %v, wantErr %v", err, tt.wantErr) 299 | return 300 | } 301 | if !reflect.DeepEqual(got, tt.want) { 302 | t.Errorf("Signature() got = %v, want %v", got, tt.want) 303 | } 304 | }) 305 | } 306 | } 307 | 308 | func TestNetwork_SwitchChainHash(t *testing.T) { 309 | type fields struct { 310 | chainHash string 311 | publicKey kyber.Point 312 | scheme *crypto.Scheme 313 | period time.Duration 314 | genesis int64 315 | fixedSig []byte 316 | } 317 | type args struct { 318 | c string 319 | } 320 | tests := []struct { 321 | name string 322 | fields fields 323 | args args 324 | wantErr bool 325 | }{ 326 | // TODO: Add test cases. 327 | } 328 | for _, tt := range tests { 329 | t.Run(tt.name, func(t *testing.T) { 330 | n := &Network{ 331 | chainHash: tt.fields.chainHash, 332 | publicKey: tt.fields.publicKey, 333 | scheme: tt.fields.scheme, 334 | period: tt.fields.period, 335 | genesis: tt.fields.genesis, 336 | fixedSig: tt.fields.fixedSig, 337 | } 338 | if err := n.SwitchChainHash(tt.args.c); (err != nil) != tt.wantErr { 339 | t.Errorf("SwitchChainHash() error = %v, wantErr %v", err, tt.wantErr) 340 | } 341 | }) 342 | } 343 | } 344 | 345 | func TestNewNetwork(t *testing.T) { 346 | type args struct { 347 | chainHash string 348 | publicKey kyber.Point 349 | sch *crypto.Scheme 350 | period time.Duration 351 | genesis int64 352 | sig []byte 353 | } 354 | tests := []struct { 355 | name string 356 | args args 357 | want *Network 358 | wantErr bool 359 | }{ 360 | // TODO: Add test cases. 361 | } 362 | for _, tt := range tests { 363 | t.Run(tt.name, func(t *testing.T) { 364 | got, err := NewNetwork(tt.args.chainHash, tt.args.publicKey, tt.args.sch, tt.args.period, tt.args.genesis, tt.args.sig) 365 | if (err != nil) != tt.wantErr { 366 | t.Errorf("NewNetwork() error = %v, wantErr %v", err, tt.wantErr) 367 | return 368 | } 369 | if !reflect.DeepEqual(got, tt.want) { 370 | t.Errorf("NewNetwork() got = %v, want %v", got, tt.want) 371 | } 372 | }) 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /networks/http/http.go: -------------------------------------------------------------------------------- 1 | // Package http implements the Network interface for the tlock package. 2 | package http 3 | 4 | import ( 5 | "context" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "strings" 14 | "time" 15 | 16 | chain "github.com/drand/drand/v2/common" 17 | "github.com/drand/drand/v2/crypto" 18 | 19 | dhttp "github.com/drand/go-clients/client/http" 20 | dclient "github.com/drand/go-clients/drand" 21 | "github.com/drand/kyber" 22 | ) 23 | 24 | // timeout represents the maximum amount of time to wait for network operations. 25 | const timeout = 5 * time.Second 26 | 27 | // ErrNotUnchained represents an error when the informed chain belongs to a 28 | // chained network. 29 | var ErrNotUnchained = errors.New("not an unchained network") 30 | 31 | // ============================================================================= 32 | 33 | // Network represents the network support using the drand http client. 34 | type Network struct { 35 | chainHash string 36 | host string 37 | client dclient.Client 38 | publicKey kyber.Point 39 | scheme crypto.Scheme 40 | period time.Duration 41 | genesis int64 42 | } 43 | 44 | // NewNetwork constructs a network for use that will use the http client. 45 | func NewNetwork(host string, chainHash string) (*Network, error) { 46 | if !strings.HasPrefix(host, "http") { 47 | host = "https://" + host 48 | } 49 | _, err := url.Parse(host + "/" + chainHash) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | hash, err := hex.DecodeString(chainHash) 55 | if err != nil { 56 | return nil, fmt.Errorf("decoding chain hash: %w", err) 57 | } 58 | 59 | client, err := dhttp.New(context.Background(), nil, host, hash, transport()) 60 | if err != nil { 61 | return nil, fmt.Errorf("creating client: %w", err) 62 | } 63 | 64 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 65 | defer cancel() 66 | 67 | info, err := client.Info(ctx) 68 | if err != nil { 69 | return nil, fmt.Errorf("getting client information: %w", err) 70 | } 71 | 72 | if info.HashString() != chainHash { 73 | return nil, fmt.Errorf("chain hash mistmatch: (requested) %s!=%s (received)", chainHash, info.HashString()) 74 | } 75 | 76 | sch, err := crypto.SchemeFromName(info.Scheme) 77 | if err != nil { 78 | return nil, ErrNotUnchained 79 | } 80 | 81 | if sch.Name == crypto.DefaultSchemeID { 82 | return nil, ErrNotUnchained 83 | } 84 | 85 | network := Network{ 86 | chainHash: chainHash, 87 | host: host, 88 | client: client, 89 | publicKey: info.PublicKey, 90 | scheme: *sch, 91 | period: info.Period, 92 | genesis: info.GenesisTime, 93 | } 94 | 95 | return &network, nil 96 | } 97 | 98 | // ChainHash returns the chain hash for this network. 99 | func (n *Network) ChainHash() string { 100 | return n.chainHash 101 | } 102 | 103 | // Current returns the current round for that network at the given date. 104 | func (n *Network) Current(date time.Time) uint64 { 105 | return chain.CurrentRound(date.Unix(), n.period, n.genesis) 106 | } 107 | 108 | // PublicKey returns the kyber point needed for encryption and decryption. 109 | func (n *Network) PublicKey() kyber.Point { 110 | return n.publicKey 111 | } 112 | 113 | // Scheme returns the drand crypto Scheme used by the network. 114 | func (n *Network) Scheme() crypto.Scheme { 115 | return n.scheme 116 | } 117 | 118 | // Signature makes a call to the network to retrieve the signature for the 119 | // specified round number. 120 | func (n *Network) Signature(roundNumber uint64) ([]byte, error) { 121 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 122 | defer cancel() 123 | 124 | result, err := n.client.Get(ctx, roundNumber) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | return result.GetSignature(), nil 130 | } 131 | 132 | // RoundNumber will return the latest round of randomness that is available 133 | // for the specified time. To handle a duration construct time like this: 134 | // time.Now().Add(6*time.Second) 135 | func (n *Network) RoundNumber(t time.Time) uint64 { 136 | return n.client.RoundAt(t) 137 | } 138 | 139 | // SwitchChainHash allows to start using another chainhash on the same host network 140 | func (n *Network) SwitchChainHash(new string) error { 141 | test, err := NewNetwork(n.host, new) 142 | if err != nil { 143 | return err 144 | } 145 | *n = *test 146 | return nil 147 | } 148 | 149 | // ============================================================================= 150 | 151 | // transport sets reasonable defaults for the connection. 152 | func transport() *http.Transport { 153 | return &http.Transport{ 154 | Proxy: http.ProxyFromEnvironment, 155 | DialContext: (&net.Dialer{ 156 | Timeout: timeout, 157 | KeepAlive: 5 * time.Second, 158 | }).DialContext, 159 | ForceAttemptHTTP2: true, 160 | MaxIdleConns: 2, 161 | IdleConnTimeout: 5 * time.Second, 162 | TLSHandshakeTimeout: 5 * time.Second, 163 | ExpectContinueTimeout: 2 * time.Second, 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /networks/http/http_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNetwork_ChainHash(t *testing.T) { 10 | if testing.Short() { 11 | t.Skip("skipping interactive network tests in short mode.") 12 | } 13 | 14 | tests := []struct { 15 | name string 16 | host string 17 | want string 18 | shouldError bool 19 | }{ 20 | { 21 | "quicknet", 22 | "api.drand.sh", 23 | "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971", 24 | false, 25 | }, 26 | { 27 | "quicknet-t", 28 | "http://pl-eu.testnet.drand.sh", 29 | "cc9c398442737cbd141526600919edd69f1d6f9b4adb67e4d912fbc64341a9a5", 30 | false, 31 | }, 32 | { 33 | "evmnet", 34 | "https://api2.drand.sh", 35 | "04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3", 36 | false, 37 | }, 38 | { 39 | "default", 40 | "https://api2.drand.sh", 41 | "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce", 42 | true, 43 | }, 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | n, err := NewNetwork(tt.host, tt.want) 48 | if tt.shouldError { 49 | require.Error(t, err) 50 | return 51 | } 52 | require.NoError(t, err) 53 | if got := n.ChainHash(); got != tt.want { 54 | t.Errorf("ChainHash() = %v, want %v", got, tt.want) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /testdata/data.txt: -------------------------------------------------------------------------------- 1 | semper dignissim. Proin eros lectus, semper non magna aliquet, iaculis ullamcorper neque. Pellentesque nisi ex, finibus et nisi nec, sodales sollicitudin magna. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent a nisi nunc. Vestibulum in pretium metus. Aliquam semper orci non sollicitudin commodo. Nam posuere nunc non scelerisque interdum. 2 | 3 | Vivamus malesuada condimentum enim vel vulputate. Morbi lobortis dui quam, vitae sagittis diam pharetra a. Proin ac pretium metus. Nullam nulla libero, ultrices ac lorem quis, condimentum vestibulum sapien. Maecenas dignissim condimentum dapibus. Curabitur iaculis pulvinar turpis, a tincidunt ante gravida vitae. Aenean in sem turpis. Mauris porta mauris id arcu ultrices dictum. Aenean pretium mi a ante sagittis, et blandit est placerat. Morbi ac mi eros turpis. -------------------------------------------------------------------------------- /testdata/decryptedFile.bin: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | # ============================================================================== 4 | # Local support 5 | 6 | run-encrypt: 7 | go run app/tle/main.go -n="http://pl-us.testnet.drand.sh/" -c="7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf" -D=30s -o=encryptedFile makefile 8 | 9 | run-decrypt: 10 | go run app/tle/main.go -d -n="http://pl-us.testnet.drand.sh/" -o=decryptedFile encryptedFile 11 | 12 | run-encrypt-a: 13 | go run app/tle/main.go -a -n="http://pl-us.testnet.drand.sh/" -c="7672797f548f3f4748ac4bf3352fc6c6b6468c9ad40ad456a397545c6e2df5bf" -D=30s -o=encryptedArmor.pem makefile 14 | 15 | run-decrypt-a: 16 | go run app/tle/main.go -d -n="http://pl-us.testnet.drand.sh/" -o=decryptedArmor.pem encryptedArmor.pem 17 | 18 | 19 | # ============================================================================== 20 | # Modules support 21 | 22 | tidy: 23 | go mod tidy 24 | go mod vendor 25 | 26 | deps-upgrade: 27 | go get -u -v ./... 28 | go mod tidy 29 | go mod vendor -------------------------------------------------------------------------------- /testdata/encryptedFile.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drand/tlock/92cbd353e79e68605653dc587909f33db69d7a08/testdata/encryptedFile.bin -------------------------------------------------------------------------------- /testdata/lorem-tle-testnet-quicknet-t-2024-01-17-15-28.tle: -------------------------------------------------------------------------------- 1 | -----BEGIN AGE ENCRYPTED FILE----- 2 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDU0MjMxNDIgY2M5YzM5ODQ0 3 | MjczN2NiZDE0MTUyNjYwMDkxOWVkZDY5ZjFkNmY5YjRhZGI2N2U0ZDkxMmZiYzY0 4 | MzQxYTlhNQpoek0rRzZyMFgvcjdxc0tlUnlyZ2wwbUc2ZFlDajdTeFhNUncvY2ZV 5 | RWhNWE0vYklaOGU4VnUxU3RxNkZHV3RMCkRSVnVaYm9BT0xQR1VoQVhzNjdReEY4 6 | eEFSMjBveWJNZGZUMHFOZU43U1J4V0ZQU1hYck80dTZZcUtBZGl3MGcKaG9tU3BK 7 | dTdqZnAzVnZpb0JKTVA1WXlaYzVqM0wrakhMYU9xdHBoUExKdwotLS0gdEJwcFZR 8 | VUdabkVMVTF6TWFMb040VHp4Wmp2RFMxM29zTjVvQ0ppN1d0bwr0aKdwOuhX9epf 9 | ewK2KOYCOM6p+cEzMFxYGUGjjui/NiDG4h8ih0fdr+N3Ig81+GH3h9KcfdkbZHV6 10 | L+hEGVZvAwWcOXi45iSCfUQO6ID4YA9/4AuxjnlSnwXFKcdwc4lMyhQdKE2Fe9j2 11 | SHG++eFe7fQi9M64ic3FOcNcrBXDNCmv7qUhSBxW8fFZjoJER5+oZd61yArLldMh 12 | KWByL5iyg83cSfB79e9mHnAk/Y4fznlJzkgVc8eh9CWvY2rtVufwmUYZoBKnFd80 13 | M0FwnrvLDZmujUpiikZopEql4x8OUxhkrWfqoFfInUWnekP/rNbYhgs2ySS33/5C 14 | 4K1ppS070rMysVoFMzpyiGFmQymx0JPh+vQ+kPGjtTrN2KplQ7XRMNehNeDjc++i 15 | fukz/ILwOvoWZBlHdS/IWR6TS4lK6PEQaRBq8jOX5zNOIdTCNfjy2i9JVuQwLA3l 16 | dz6+JWEiq9k25MPciF6zKvsxkmDvlamacmu2SXoo11lkW7o1QlWG8602+Uk4FYi8 17 | HukJ8PHWAxC3fJf1Tb0EeUIcKG2hbXkzCOb/KlmsYZpPeDgoYS1WGiOlvXorjqHI 18 | Cv0y2K+LakpgFM17whY47rpf+gwPbbf5i+wOCSQ31rlgECjJbrEctVaGIohCEDhF 19 | xh+MQcPzAkKgTPWkd7xEFJyAch87V4/crCVYGBWRkB+oG+g3l5RHcANKtDmdGMCK 20 | nLZ98aAXr4UIl/dLloctp+akmjUN0lYbbHWeYN+Qp1WwpD4j3xPgO9k7kHLR9gR1 21 | 9eYX/Esk9AaAjcLWT041WH3CsrfMBh1Hm5zA0HEa0mcKjNPfPA+7U6yix8JSiDj3 22 | bG4Sb+jtHdQfISpGM+Bo9VatM1kFTMSVTPDsuSTRxuOdfUdk4sum4AxWuug5u+AN 23 | ffHVI7+RCHguatWuxYNlgEcgQ2EX2+RsBjbTjmUD+nNGRzMEh9MdFwdImhlgwUin 24 | zLZXP6MAsri7YOTWiD21zJ5shxaPhWi2ERDrnjYTs9P95BPiT025gMn/MMHcqqpz 25 | hNVN7hY8TpaKxXm9DErYD5h+qs87zVX24isfxw1Ix0eX42+tU1CbSRoG/NbxMdQV 26 | fq/Aeim86Fq7Q3BsrlJ6YfB8bMILSB2VfgfbWwIkg2972LZttgTLk1Q4eeZGODUL 27 | RgqTJ7w6GTdHDphbjckAo5JmRlHTt7Bl2vnRZIrsD09I+hmkT8NfRc9R3Gy4q6jD 28 | jroKDtZJzOahH9pGQAZoR2714upgZK5541KgA4bbeMnPb84+1g+FxEj6q/t1uCgv 29 | 3WhuJtmwHAbRw5abwJz+HkiXBWYfp2oyVG2ZJ+XjQFcnYiYx2HSm73ly1ICS36Qq 30 | rw4ve/c4w5gUoOZJ+YTmoWJHdmKo6GbD+OV3yBOUbhMu/mUyKWwDyFZE/TYNBV1y 31 | yFtROCURe6axDDcOUgR+QEsl8oKXA6rppMwIeMg= 32 | -----END AGE ENCRYPTED FILE----- 33 | -------------------------------------------------------------------------------- /testdata/lorem.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse arcu nisi, ornare sed risus quis, scelerisque molestie massa. Integer ullamcorper in ex non accumsan. Sed bibendum feugiat velit eu tempus. Vivamus ultrices, nisi ut dictum tempus, eros turpis pretium mi, a feugiat justo lorem sit amet odio. Integer dignissim vestibulum consectetur. Nunc bibendum lacinia nisi viverra vehicula. Nam eget euismod tortor, eget efficitur neque. Praesent lacus nulla, iaculis sed augue vitae, congue dignissim neque. Aliquam ultricies lorem ligula, quis pretium nunc convallis sed. Sed eget varius mauris. Vivamus nulla lectus, varius nec fermentum eget, laoreet quis mauris. Mauris non lectus sit amet ligula interdum auctor vel sit amet nisl. Sed vel tincidunt leo, vitae pellentesque lectus. Suspendisse cursus, neque a molestie rhoncus, lectus odio fermentum erat, a feugiat nibh eros in lectus. Phasellus finibus mauris sodales, ullamcorper neque eget, sodales erat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. 2 | -------------------------------------------------------------------------------- /tlock.go: -------------------------------------------------------------------------------- 1 | // Package tlock provides an API for encrypting/decrypting data using 2 | // drand timelock encryption. This allows data to be encrypted and only 3 | // decrypted in the future. 4 | package tlock 5 | 6 | import ( 7 | "bufio" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "time" 12 | 13 | "filippo.io/age" 14 | "filippo.io/age/armor" 15 | chain "github.com/drand/drand/v2/common" 16 | "github.com/drand/drand/v2/crypto" 17 | "github.com/drand/kyber" 18 | bls "github.com/drand/kyber-bls12381" 19 | "github.com/drand/kyber/encrypt/ibe" 20 | bn "github.com/drand/kyber/pairing/bn254" 21 | "gopkg.in/yaml.v3" 22 | ) 23 | 24 | // ErrTooEarly represents an error when a decryption operation happens early. 25 | var ErrTooEarly = errors.New("too early to decrypt") 26 | var ErrInvalidPublicKey = errors.New("the public key received from the network to encrypt this was infinity and thus insecure") 27 | 28 | // ============================================================================= 29 | 30 | // Network represents a system that provides support for encrypting/decrypting 31 | // a DEK based on a future time. 32 | type Network interface { 33 | ChainHash() string 34 | Current(time.Time) uint64 35 | PublicKey() kyber.Point 36 | Scheme() crypto.Scheme 37 | Signature(roundNumber uint64) ([]byte, error) 38 | SwitchChainHash(string) error 39 | } 40 | 41 | // ============================================================================= 42 | 43 | // Tlock provides an API for timelock encryption and decryption. 44 | type Tlock struct { 45 | network Network 46 | trustChainhash bool 47 | } 48 | 49 | // New constructs a tlock for the specified network which can encrypt data that 50 | // can be decrypted until the future. By default a new network will trust the 51 | // chainhash it sees in ciphertexts and try and use these unless Strict was 52 | // called to prevent it. 53 | func New(network Network) Tlock { 54 | return Tlock{ 55 | network: network, 56 | trustChainhash: true, 57 | } 58 | } 59 | 60 | func (t Tlock) Strict() Tlock { 61 | t.trustChainhash = false 62 | return t 63 | } 64 | 65 | // Encrypt will encrypt the source and write that to the destination. The encrypted 66 | // data will not be decryptable until the specified round is reached by the network. 67 | func (t Tlock) Encrypt(dst io.Writer, src io.Reader, roundNumber uint64) (err error) { 68 | w, err := age.Encrypt(dst, &Recipient{network: t.network, roundNumber: roundNumber}) 69 | if err != nil { 70 | return fmt.Errorf("hybrid encrypt: %w", err) 71 | } 72 | 73 | defer func() { 74 | if err = w.Close(); err != nil { 75 | err = fmt.Errorf("close: %w", err) 76 | } 77 | }() 78 | 79 | if _, err := io.Copy(w, src); err != nil { 80 | return fmt.Errorf("write: %w", err) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | // Decrypt will decrypt the source and write that to the destination. The decrypted 87 | // data will not be decryptable unless the specified round from the encrypt call 88 | // is reached by the network. 89 | func (t Tlock) Decrypt(dst io.Writer, src io.Reader) error { 90 | rr := bufio.NewReader(src) 91 | 92 | if start, _ := rr.Peek(len(armor.Header)); string(start) == armor.Header { 93 | src = armor.NewReader(rr) 94 | } else { 95 | src = rr 96 | } 97 | 98 | r, err := age.Decrypt(src, &Identity{network: t.network, trustChainhash: t.trustChainhash}) 99 | if err != nil { 100 | return fmt.Errorf("hybrid decrypt: %w", err) 101 | } 102 | 103 | if _, err := io.Copy(dst, r); err != nil { 104 | return fmt.Errorf("write: %w", err) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | // Metadata will return details about the drand network 111 | func (t Tlock) Metadata(dst io.Writer) (err error) { 112 | type Metadata struct { 113 | ChainHash string `yaml:"chain_hash"` 114 | Current uint64 `yaml:"current"` 115 | PublicKey string `yaml:"public_key"` 116 | Scheme string `yaml:"scheme"` 117 | } 118 | scheme := t.network.Scheme() 119 | metadata := Metadata{ 120 | ChainHash: t.network.ChainHash(), 121 | Current: t.network.Current(time.Now()), 122 | PublicKey: t.network.PublicKey().String(), 123 | Scheme: scheme.String(), 124 | } 125 | metadataBytes, err := yaml.Marshal(metadata) 126 | if err != nil { 127 | return fmt.Errorf("error marshalling metadata: %w", err) 128 | } 129 | if _, err := dst.Write(metadataBytes); err != nil { 130 | return fmt.Errorf("error writing metadata: %w", err) 131 | } 132 | return nil 133 | } 134 | 135 | // ============================================================================= 136 | 137 | // TimeLock encrypts the specified data for the given round number. The data 138 | // can't be decrypted until the specified round is reached by the network in use. 139 | func TimeLock(scheme crypto.Scheme, publicKey kyber.Point, roundNumber uint64, data []byte) (*ibe.Ciphertext, error) { 140 | if publicKey.Equal(publicKey.Clone().Null()) { 141 | return nil, ErrInvalidPublicKey 142 | } 143 | 144 | id := scheme.DigestBeacon(&chain.Beacon{ 145 | Round: roundNumber, 146 | }) 147 | 148 | var cipherText *ibe.Ciphertext 149 | var err error 150 | switch scheme.Name { 151 | case crypto.ShortSigSchemeID: 152 | // the ShortSigSchemeID uses the wrong DST for G1, so we keep it for retro-compatibility 153 | cipherText, err = ibe.EncryptCCAonG2(bls.NewBLS12381SuiteWithDST(bls.DefaultDomainG2(), bls.DefaultDomainG2()), publicKey, id, data) 154 | case crypto.UnchainedSchemeID: 155 | cipherText, err = ibe.EncryptCCAonG1(bls.NewBLS12381Suite(), publicKey, id, data) 156 | case crypto.SigsOnG1ID: 157 | cipherText, err = ibe.EncryptCCAonG2(bls.NewBLS12381Suite(), publicKey, id, data) 158 | case crypto.BN254UnchainedOnG1SchemeID: 159 | suite := bn.NewSuiteBn254() 160 | suite.SetDomainG1([]byte("BLS_SIG_BN254G1_XMD:KECCAK-256_SVDW_RO_NUL_")) 161 | suite.SetDomainG2([]byte("BLS_SIG_BN254G2_XMD:KECCAK-256_SVDW_RO_NUL_")) 162 | cipherText, err = ibe.EncryptCCAonG2(suite, publicKey, id, data) 163 | default: 164 | return nil, fmt.Errorf("unsupported drand scheme '%s'", scheme.Name) 165 | } 166 | 167 | if err != nil { 168 | return nil, fmt.Errorf("encrypt data: %w", err) 169 | } 170 | 171 | return cipherText, nil 172 | } 173 | 174 | // TimeUnlock decrypts the specified ciphertext for the given beacon. The 175 | // ciphertext can't be decrypted until the specified round is reached by the network in use. 176 | func TimeUnlock(scheme crypto.Scheme, publicKey kyber.Point, beacon chain.Beacon, ciphertext *ibe.Ciphertext) ([]byte, error) { 177 | if err := scheme.VerifyBeacon(&beacon, publicKey); err != nil { 178 | return nil, fmt.Errorf("verify beacon: %w", err) 179 | } 180 | 181 | var data []byte 182 | var err error 183 | switch scheme.Name { 184 | case crypto.ShortSigSchemeID: 185 | var signature bls.KyberG1 186 | if err := signature.UnmarshalBinary(beacon.Signature); err != nil { 187 | return nil, fmt.Errorf("unmarshal kyber G1: %w", err) 188 | } 189 | // the ShortSigSchemeID uses the wrong DST for G1, so we keep it for retro-compatibility 190 | data, err = ibe.DecryptCCAonG2(bls.NewBLS12381SuiteWithDST(bls.DefaultDomainG2(), bls.DefaultDomainG2()), &signature, ciphertext) 191 | case crypto.UnchainedSchemeID: 192 | var signature bls.KyberG2 193 | if err := signature.UnmarshalBinary(beacon.Signature); err != nil { 194 | return nil, fmt.Errorf("unmarshal kyber G2: %w", err) 195 | } 196 | data, err = ibe.DecryptCCAonG1(bls.NewBLS12381Suite(), &signature, ciphertext) 197 | case crypto.SigsOnG1ID: 198 | var signature bls.KyberG1 199 | if err := signature.UnmarshalBinary(beacon.Signature); err != nil { 200 | return nil, fmt.Errorf("unmarshal kyber G1: %w", err) 201 | } 202 | data, err = ibe.DecryptCCAonG2(bls.NewBLS12381Suite(), &signature, ciphertext) 203 | case crypto.BN254UnchainedOnG1SchemeID: 204 | suite := bn.NewSuiteBn254() 205 | suite.SetDomainG1([]byte("BLS_SIG_BN254G1_XMD:KECCAK-256_SVDW_RO_NUL_")) 206 | suite.SetDomainG2([]byte("BLS_SIG_BN254G2_XMD:KECCAK-256_SVDW_RO_NUL_")) 207 | signature := suite.G1().Point() 208 | if err := signature.UnmarshalBinary(beacon.Signature); err != nil { 209 | return nil, fmt.Errorf("unmarshal kyber G1: %w", err) 210 | } 211 | data, err = ibe.DecryptCCAonG2(suite, signature, ciphertext) 212 | default: 213 | return nil, fmt.Errorf("unsupported drand scheme '%s'", scheme.Name) 214 | } 215 | 216 | if err != nil { 217 | return nil, fmt.Errorf("decrypt dek: %w", err) 218 | } 219 | 220 | return data, nil 221 | } 222 | 223 | // ============================================================================= 224 | 225 | // These constants define the size of the different CipherDEK fields. 226 | const ( 227 | cipherVLen = 16 228 | cipherWLen = 16 229 | ) 230 | 231 | // CiphertextToBytes converts a ciphertext value to a set of bytes. 232 | func CiphertextToBytes(scheme crypto.Scheme, ciphertext *ibe.Ciphertext) ([]byte, error) { 233 | kyberPoint, err := ciphertext.U.MarshalBinary() 234 | if err != nil { 235 | return nil, fmt.Errorf("marshal kyber point: %w", err) 236 | } 237 | 238 | kyberPointLen := ciphertext.U.MarshalSize() 239 | if kyberPointLen != scheme.KeyGroup.PointLen() { 240 | return nil, fmt.Errorf("unsupported type (MarshalSize %d) for U: %T", kyberPointLen, ciphertext.U) 241 | } 242 | 243 | b := make([]byte, kyberPointLen+cipherVLen+cipherWLen) 244 | copy(b, kyberPoint) 245 | copy(b[kyberPointLen:], ciphertext.V) 246 | copy(b[kyberPointLen+cipherVLen:], ciphertext.W) 247 | 248 | return b, nil 249 | } 250 | 251 | // BytesToCiphertext converts bytes to a ciphertext. 252 | func BytesToCiphertext(scheme crypto.Scheme, b []byte) (*ibe.Ciphertext, error) { 253 | kyberPointLen := scheme.KeyGroup.PointLen() 254 | if tot := kyberPointLen + cipherVLen + cipherWLen; len(b) != tot { 255 | return nil, fmt.Errorf("incorrect length: exp: %d got: %d", tot, len(b)) 256 | } 257 | 258 | kyberPoint := make([]byte, kyberPointLen) 259 | copy(kyberPoint, b[:kyberPointLen]) 260 | 261 | cipherV := make([]byte, cipherVLen) 262 | copy(cipherV, b[kyberPointLen:kyberPointLen+cipherVLen]) 263 | 264 | cipherW := make([]byte, cipherVLen) 265 | copy(cipherW, b[kyberPointLen+cipherVLen:]) 266 | if len(b[kyberPointLen+cipherVLen:]) != cipherVLen { 267 | return nil, fmt.Errorf("invalid ciphertext length: %d", len(b[kyberPointLen+cipherVLen:])) 268 | } 269 | 270 | u := scheme.KeyGroup.Point() 271 | if err := u.UnmarshalBinary(kyberPoint); err != nil { 272 | return nil, fmt.Errorf("unmarshal kyber point (type %T): %w", scheme.KeyGroup, err) 273 | } 274 | 275 | ct := ibe.Ciphertext{ 276 | U: u, 277 | V: cipherV, 278 | W: cipherW, 279 | } 280 | 281 | return &ct, nil 282 | } 283 | -------------------------------------------------------------------------------- /tlock_age.go: -------------------------------------------------------------------------------- 1 | package tlock 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "filippo.io/age" 13 | chain "github.com/drand/drand/v2/common" 14 | ) 15 | 16 | var ErrWrongChainhash = errors.New("invalid chainhash") 17 | 18 | // Recipient implements the age Recipient interface. This is used to encrypt 19 | // data with the age Encrypt API. 20 | type Recipient struct { 21 | network Network 22 | roundNumber uint64 23 | } 24 | 25 | func NewRecipient(network Network, roundNumber uint64) *Recipient { 26 | return &Recipient{ 27 | network: network, 28 | roundNumber: roundNumber, 29 | } 30 | } 31 | 32 | func (t *Recipient) SetNetwork(network Network) { 33 | t.network = network 34 | } 35 | 36 | // SetRound allows you to set the round number towards which you'd like to timelock encrypt data 37 | // the Wrap process will then fetch the public key and appropriate scheme from the Recipient's 38 | // network and Wrap filekeys toward that round number using timelock encryption. 39 | func (t *Recipient) SetRound(round uint64) { 40 | t.roundNumber = round 41 | } 42 | 43 | // Wrap is called by the age Encrypt API and is provided the DEK generated by 44 | // age that is used for encrypting/decrypting data. Inside of Wrap we encrypt 45 | // the DEK using timelock encryption. 46 | func (t *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { 47 | ciphertext, err := TimeLock(t.network.Scheme(), t.network.PublicKey(), t.roundNumber, fileKey) 48 | if err != nil { 49 | return nil, fmt.Errorf("encrypt dek: %w", err) 50 | } 51 | 52 | body, err := CiphertextToBytes(t.network.Scheme(), ciphertext) 53 | if err != nil { 54 | return nil, fmt.Errorf("bytes: %w", err) 55 | } 56 | 57 | stanza := age.Stanza{ 58 | Type: "tlock", 59 | Args: []string{strconv.FormatUint(t.roundNumber, 10), t.network.ChainHash()}, 60 | Body: body, 61 | } 62 | 63 | return []*age.Stanza{&stanza}, nil 64 | } 65 | 66 | func (t *Recipient) String() string { 67 | sb := strings.Builder{} 68 | 69 | sb.WriteString(fmt.Sprintf("%d@", t.roundNumber)) 70 | sb.WriteString(t.network.ChainHash()) 71 | sb.WriteString("-" + t.network.Scheme().Name) 72 | d, err := t.network.PublicKey().MarshalBinary() 73 | if err != nil { 74 | d = []byte("error") 75 | } 76 | sb.WriteString("-" + hex.EncodeToString(d)) 77 | 78 | return sb.String() 79 | } 80 | 81 | // ============================================================================= 82 | 83 | // Identity implements the age Identity interface. This is used to decrypt 84 | // data with the age Decrypt API. 85 | type Identity struct { 86 | network Network 87 | trustChainhash bool 88 | } 89 | 90 | func NewIdentity(network Network, trustChainhash bool) *Identity { 91 | return &Identity{ 92 | network: network, 93 | trustChainhash: trustChainhash, 94 | } 95 | } 96 | 97 | func (t *Identity) SetNetwork(network Network) { 98 | t.network = network 99 | } 100 | 101 | func (t *Identity) SetTrust(trust bool) { 102 | t.trustChainhash = trust 103 | } 104 | 105 | // Unwrap is called by the age Decrypt API and is provided the DEK that was time 106 | // lock encrypted by the Wrap function via the Stanza. Inside of Unwrap we decrypt 107 | // the DEK and provide back to age. If the ciphertext uses a chainhash different 108 | // from the one we are current using, we will try switching to it. 109 | func (t *Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) { 110 | if len(stanzas) < 1 { 111 | return nil, errors.New("check stanzas length: should be at least one") 112 | } 113 | 114 | invalid := "" 115 | for _, stanza := range stanzas { 116 | if stanza.Type != "tlock" { 117 | continue 118 | } 119 | 120 | if len(stanza.Args) != 2 { 121 | continue 122 | } 123 | 124 | roundNumber, err := strconv.ParseUint(stanza.Args[0], 10, 64) 125 | if err != nil { 126 | return nil, fmt.Errorf("parse block round: %w", err) 127 | } 128 | 129 | if t.network.ChainHash() != stanza.Args[1] { 130 | invalid = stanza.Args[1] 131 | if t.trustChainhash { 132 | fmt.Fprintf(os.Stderr, "WARN: stanza using different chainhash '%s', trying to use it instead.\n", invalid) 133 | err = t.network.SwitchChainHash(invalid) 134 | if err != nil { 135 | continue 136 | } 137 | } else { 138 | continue 139 | } 140 | } 141 | 142 | ciphertext, err := BytesToCiphertext(t.network.Scheme(), stanza.Body) 143 | if err != nil { 144 | return nil, fmt.Errorf("parse cipher dek: %w", err) 145 | } 146 | 147 | signature, err := t.network.Signature(roundNumber) 148 | if err != nil { 149 | return nil, fmt.Errorf( 150 | "%w: expected round %d > %d current round", 151 | ErrTooEarly, 152 | roundNumber, 153 | t.network.Current(time.Now())) 154 | } 155 | 156 | beacon := chain.Beacon{ 157 | Round: roundNumber, 158 | Signature: signature, 159 | } 160 | 161 | fileKey, err := TimeUnlock(t.network.Scheme(), t.network.PublicKey(), beacon, ciphertext) 162 | if err != nil { 163 | return nil, fmt.Errorf("decrypt dek: %w", err) 164 | } 165 | 166 | return fileKey, nil 167 | } 168 | 169 | if len(invalid) > 0 { 170 | return nil, fmt.Errorf("%w: current network uses %s != %s the ciphertext requires.\n"+ 171 | "Note that is might have been encrypted using our testnet instead", ErrWrongChainhash, t.network.ChainHash(), invalid) 172 | } 173 | 174 | return nil, fmt.Errorf("check stanza type: wrong type: %w", age.ErrIncorrectIdentity) 175 | } 176 | 177 | func (t *Identity) String() string { 178 | sb := strings.Builder{} 179 | 180 | sb.WriteString(fmt.Sprintf("Trust:%v@", t.trustChainhash)) 181 | sb.WriteString(t.network.ChainHash()) 182 | sb.WriteString("-" + t.network.Scheme().Name) 183 | sb.WriteString("-" + t.network.PublicKey().String()) 184 | 185 | return sb.String() 186 | } 187 | -------------------------------------------------------------------------------- /tlock_age_test.go: -------------------------------------------------------------------------------- 1 | package tlock 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "testing" 7 | "time" 8 | 9 | "github.com/drand/tlock/networks/http" 10 | ) 11 | 12 | const ( 13 | testnetHost = "http://pl-us.testnet.drand.sh/" 14 | testnetChainHash = "ddb3665060932c267aacde99049ea31f3f5a049b1741c31cf71cd5d7d11a8da2" 15 | ) 16 | 17 | func Test_WrapUnwrap(t *testing.T) { 18 | network, err := http.NewNetwork(testnetHost, testnetChainHash) 19 | if err != nil { 20 | t.Fatalf("network error %s", err) 21 | } 22 | 23 | recipient := Recipient{ 24 | roundNumber: network.RoundNumber(time.Now()), 25 | network: network, 26 | } 27 | 28 | // 16 is the constant fileKeySize 29 | fileKey := make([]byte, 16) 30 | if _, err := rand.Read(fileKey); err != nil { 31 | t.Fatalf("rand read filekey: %s", err) 32 | } 33 | 34 | stanza, err := recipient.Wrap(fileKey) 35 | if err != nil { 36 | t.Fatalf("wrap error %s", err) 37 | } 38 | 39 | identity := Identity{ 40 | network: network, 41 | } 42 | 43 | b, err := identity.Unwrap(stanza) 44 | if err != nil { 45 | t.Fatalf("unwrap error %s", err) 46 | } 47 | 48 | if !bytes.Equal(b, fileKey) { 49 | t.Fatalf("decrypted filekey is invalid; expected %d; got %d", len(b), len(fileKey)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tlock_test.go: -------------------------------------------------------------------------------- 1 | package tlock_test 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" // Calls init function. 6 | "encoding/hex" 7 | "errors" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | chain "github.com/drand/drand/v2/common" 15 | "github.com/drand/drand/v2/crypto" 16 | bls "github.com/drand/kyber-bls12381" 17 | "github.com/drand/tlock" 18 | "github.com/drand/tlock/networks/fixed" 19 | "github.com/drand/tlock/networks/http" 20 | 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | var ( 25 | //go:embed testdata/data.txt 26 | dataFile []byte 27 | //go:embed testdata/lorem.txt 28 | loremBytes []byte 29 | ) 30 | 31 | const ( 32 | testnetHost = "http://pl-us.testnet.drand.sh/" 33 | testnetUnchainedOnEVM = "ddb3665060932c267aacde99049ea31f3f5a049b1741c31cf71cd5d7d11a8da2" 34 | testnetQuicknetT = "cc9c398442737cbd141526600919edd69f1d6f9b4adb67e4d912fbc64341a9a5" 35 | mainnetHost = "http://api.drand.sh/" 36 | mainnetQuicknet = "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" 37 | mainnetEvm = "04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3" 38 | ) 39 | 40 | func TestEarlyDecryptionWithDuration(t *testing.T) { 41 | for host, hashes := range map[string][]string{testnetHost: {testnetUnchainedOnEVM, testnetQuicknetT}, 42 | mainnetHost: {mainnetQuicknet}} { 43 | for _, hash := range hashes { 44 | network, err := http.NewNetwork(host, hash) 45 | require.NoError(t, err) 46 | 47 | // ========================================================================= 48 | // Encrypt 49 | 50 | // Read the plaintext data to be encrypted. 51 | in, err := os.Open("testdata/data.txt") 52 | require.NoError(t, err) 53 | defer in.Close() 54 | 55 | // Write the encoded information to this buffer. 56 | var cipherData bytes.Buffer 57 | 58 | // Enough duration to check for a non-existent beacon. 59 | duration := 10 * time.Second 60 | 61 | roundNumber := network.RoundNumber(time.Now().Add(duration)) 62 | err = tlock.New(network).Encrypt(&cipherData, in, roundNumber) 63 | require.NoError(t, err) 64 | 65 | // ========================================================================= 66 | // Decrypt 67 | 68 | // Write the decoded information to this buffer. 69 | var plainData bytes.Buffer 70 | 71 | // We DO NOT wait for the future beacon to exist. 72 | err = tlock.New(network).Decrypt(&plainData, &cipherData) 73 | require.ErrorIs(t, err, tlock.ErrTooEarly) 74 | } 75 | } 76 | } 77 | 78 | func TestEarlyDecryptionWithRound(t *testing.T) { 79 | network, err := http.NewNetwork(testnetHost, testnetUnchainedOnEVM) 80 | require.NoError(t, err) 81 | 82 | // ========================================================================= 83 | // Encrypt 84 | 85 | // Read the plaintext data to be encrypted. 86 | in, err := os.Open("testdata/data.txt") 87 | require.NoError(t, err) 88 | defer in.Close() 89 | 90 | var cipherData bytes.Buffer 91 | futureRound := network.RoundNumber(time.Now().Add(1 * time.Minute)) 92 | 93 | err = tlock.New(network).Encrypt(&cipherData, in, futureRound) 94 | require.NoError(t, err) 95 | 96 | // ========================================================================= 97 | // Decrypt 98 | 99 | // Write the decoded information to this buffer. 100 | var plainData bytes.Buffer 101 | 102 | // We DO NOT wait for the future beacon to exist. 103 | err = tlock.New(network).Decrypt(&plainData, &cipherData) 104 | require.ErrorIs(t, err, tlock.ErrTooEarly) 105 | } 106 | 107 | func TestEncryptionWithDuration(t *testing.T) { 108 | if testing.Short() { 109 | t.Skip("skipping live testing in short mode") 110 | } 111 | 112 | network, err := http.NewNetwork(testnetHost, testnetUnchainedOnEVM) 113 | require.NoError(t, err) 114 | 115 | // ========================================================================= 116 | // Encrypt 117 | 118 | // Read the plaintext data to be encrypted. 119 | in, err := os.Open("testdata/data.txt") 120 | require.NoError(t, err) 121 | defer in.Close() 122 | 123 | // Write the encoded information to this buffer. 124 | var cipherData bytes.Buffer 125 | 126 | // Enough duration to check for a non-existent beacon. 127 | duration := 4 * time.Second 128 | 129 | roundNumber := network.RoundNumber(time.Now().Add(duration)) 130 | err = tlock.New(network).Encrypt(&cipherData, in, roundNumber) 131 | require.NoError(t, err) 132 | 133 | // ========================================================================= 134 | // Decrypt 135 | 136 | time.Sleep(5 * time.Second) 137 | 138 | // Write the decoded information to this buffer. 139 | var plainData bytes.Buffer 140 | 141 | err = tlock.New(network).Decrypt(&plainData, &cipherData) 142 | require.NoError(t, err) 143 | 144 | if !bytes.Equal(plainData.Bytes(), dataFile) { 145 | t.Fatalf("decrypted file is invalid; expected %d; got %d", len(dataFile), len(plainData.Bytes())) 146 | } 147 | } 148 | 149 | func TestDecryptVariousChainhashes(t *testing.T) { 150 | dir := "./testdata" 151 | prefix := "lorem-" 152 | 153 | files, err := os.ReadDir(dir) 154 | require.NoError(t, err) 155 | network, err := http.NewNetwork(testnetHost, testnetUnchainedOnEVM) 156 | require.NoError(t, err) 157 | 158 | for _, file := range files { 159 | if strings.HasPrefix(file.Name(), prefix) { 160 | t.Run("Decrypt-"+file.Name(), func(ts *testing.T) { 161 | filePath := filepath.Join(dir, file.Name()) 162 | cipherData, err := os.Open(filePath) 163 | require.NoError(ts, err) 164 | var plainData bytes.Buffer 165 | err = tlock.New(network).Decrypt(&plainData, cipherData) 166 | if errors.Is(err, tlock.ErrWrongChainhash) { 167 | require.Contains(ts, file.Name(), "timevault-mainnet-2024") 168 | return 169 | } 170 | 171 | require.NoError(ts, err) 172 | 173 | if !bytes.Equal(plainData.Bytes(), loremBytes) { 174 | ts.Fatalf("decrypted file is invalid; expected %d; got %d:\n %v \n %v", len(loremBytes), len(plainData.Bytes()), loremBytes, plainData.Bytes()) 175 | } 176 | }) 177 | } 178 | } 179 | } 180 | 181 | func TestDecryptStrict(t *testing.T) { 182 | dir := "./testdata" 183 | prefix := "lorem-" 184 | 185 | files, err := os.ReadDir(dir) 186 | require.NoError(t, err) 187 | network, err := http.NewNetwork(testnetHost, testnetUnchainedOnEVM) 188 | require.NoError(t, err) 189 | 190 | for _, file := range files { 191 | if strings.Contains(file.Name(), "testnet-unchained-3s-2024") { 192 | continue 193 | } 194 | if strings.Contains(file.Name(), "timevault-testnet-2024") { 195 | continue 196 | } 197 | if strings.HasPrefix(file.Name(), prefix) { 198 | t.Run("DontDecryptStrict-"+file.Name(), func(ts *testing.T) { 199 | filePath := filepath.Join(dir, file.Name()) 200 | cipherData, err := os.Open(filePath) 201 | require.NoError(ts, err) 202 | var plainData bytes.Buffer 203 | err = tlock.New(network).Strict().Decrypt(&plainData, cipherData) 204 | require.ErrorIs(ts, err, tlock.ErrWrongChainhash) 205 | }) 206 | } 207 | } 208 | } 209 | 210 | func TestEncryptionWithRound(t *testing.T) { 211 | if testing.Short() { 212 | t.Skip("skipping live testing in short mode") 213 | } 214 | 215 | network, err := http.NewNetwork(testnetHost, testnetUnchainedOnEVM) 216 | require.NoError(t, err) 217 | 218 | // ========================================================================= 219 | // Encrypt 220 | 221 | // Read the plaintext data to be encrypted. 222 | in, err := os.Open("testdata/data.txt") 223 | require.NoError(t, err) 224 | defer in.Close() 225 | 226 | // Write the encoded information to this buffer. 227 | var cipherData bytes.Buffer 228 | 229 | futureRound := network.RoundNumber(time.Now().Add(6 * time.Second)) 230 | err = tlock.New(network).Encrypt(&cipherData, in, futureRound) 231 | require.NoError(t, err) 232 | 233 | // ========================================================================= 234 | // Decrypt 235 | 236 | var plainData bytes.Buffer 237 | 238 | // Wait for the future beacon to exist. 239 | time.Sleep(10 * time.Second) 240 | 241 | err = tlock.New(network).Decrypt(&plainData, &cipherData) 242 | require.NoError(t, err) 243 | 244 | if !bytes.Equal(plainData.Bytes(), dataFile) { 245 | t.Fatalf("decrypted file is invalid; expected %d; got %d", len(dataFile), len(plainData.Bytes())) 246 | } 247 | } 248 | 249 | func TestTimeLockUnlock(t *testing.T) { 250 | if testing.Short() { 251 | t.Skip("skipping live testing in short mode") 252 | } 253 | tests := []struct { 254 | name string 255 | host string 256 | chainhash string 257 | }{ 258 | { 259 | "quicknetT", 260 | testnetHost, 261 | testnetQuicknetT, 262 | }, 263 | { 264 | "quicknet", 265 | mainnetHost, 266 | mainnetQuicknet, 267 | }, 268 | { 269 | "evmnet", 270 | mainnetHost, 271 | mainnetEvm, 272 | }, 273 | } 274 | for _, tt := range tests { 275 | t.Run(tt.name, func(t *testing.T) { 276 | network, err := http.NewNetwork(tt.host, tt.chainhash) 277 | require.NoError(t, err) 278 | 279 | futureRound := network.RoundNumber(time.Now()) 280 | 281 | id, err := network.Signature(futureRound) 282 | require.NoError(t, err) 283 | 284 | data := []byte(`anything`) 285 | 286 | cipherText, err := tlock.TimeLock(network.Scheme(), network.PublicKey(), futureRound, data) 287 | require.NoError(t, err) 288 | 289 | beacon := chain.Beacon{ 290 | Round: futureRound, 291 | Signature: id, 292 | } 293 | 294 | b, err := tlock.TimeUnlock(network.Scheme(), network.PublicKey(), beacon, cipherText) 295 | require.NoError(t, err) 296 | 297 | if !bytes.Equal(data, b) { 298 | t.Fatalf("unexpected bytes; expected len %d; got %d", len(data), len(b)) 299 | } 300 | }) 301 | } 302 | } 303 | 304 | func TestCannotEncryptWithPointAtInfinity(t *testing.T) { 305 | suite := bls.NewBLS12381Suite() 306 | t.Run("on G2", func(t *testing.T) { 307 | infinity := suite.G2().Scalar().Zero() 308 | pointAtInfinity := suite.G2().Point().Mul(infinity, nil) 309 | 310 | _, err := tlock.TimeLock(*crypto.NewPedersenBLSUnchainedG1(), pointAtInfinity, 10, []byte("deadbeef")) 311 | require.ErrorIs(t, err, tlock.ErrInvalidPublicKey) 312 | }) 313 | 314 | t.Run("on G1", func(t *testing.T) { 315 | infinity := suite.G1().Scalar().Zero() 316 | pointAtInfinity := suite.G1().Point().Mul(infinity, nil) 317 | 318 | _, err := tlock.TimeLock(*crypto.NewPedersenBLSUnchained(), pointAtInfinity, 10, []byte("deadbeef")) 319 | require.ErrorIs(t, err, tlock.ErrInvalidPublicKey) 320 | }) 321 | 322 | } 323 | 324 | func TestDecryptText(t *testing.T) { 325 | cipher := `-----BEGIN AGE ENCRYPTED FILE----- 326 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDEyMDQwODgzIDUyZGI5YmE3 327 | MGUwY2MwZjZlYWY3ODAzZGQwNzQ0N2ExZjU0Nzc3MzVmZDNmNjYxNzkyYmE5NDYw 328 | MGM4NGU5NzEKa1JjK01NSEUwS005b1V0SmNLTWZGb1JFVzBXN1JQbTNtdzZpVUJ1 329 | cGNXVkZkZDJQb1h6U0JrK25TM01BNnBKNwpHZDl3REhmVU5hTldXTWw2cGVia2Jh 330 | OUVNZGJDWnBuQVNtOWFIb3hqUitwaGFVT2xoS1ppZGl5ZHBLSStPS2N0CmxvT2ZP 331 | SW9KaGtndTVTRnJUOGVVQTJUOGk3aTBwQlBzTDlTWUJUZEJQb28KLS0tIEl6Q1Js 332 | WSt1RXp0d21CbEg0cTFVZGNJaW9pS2l0M0c0bHVxNlNjT2w3UUUKDI4cDlPHPgjy 333 | UnBmtsw6U2LlKh8iDf0E1PfwDenmKFfQaAGm0WLxdlzP8Q== 334 | -----END AGE ENCRYPTED FILE-----` 335 | 336 | t.Run("With valid network", func(tt *testing.T) { 337 | network, err := fixed.FromInfo(`{"public_key":"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a","period":3,"genesis_time":1692803367,"genesis_seed":"f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e","chain_hash":"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971","scheme":"bls-unchained-g1-rfc9380","beacon_id":"quicknet"}`) 338 | require.NoError(tt, err) 339 | sig, err := hex.DecodeString("929906c959032ab363c9f26570d215d66f5c06cb0c44fe508c12bb5839f04ec895bb6868e5b9ff13ab289bdb5266b394") 340 | require.NoError(tt, err) 341 | 342 | network.SetSignature(sig) 343 | 344 | testReader := strings.NewReader(cipher) 345 | var plainData bytes.Buffer 346 | 347 | err = tlock.New(network).Decrypt(&plainData, testReader) 348 | require.NoError(tt, err) 349 | 350 | require.Equal(tt, "hello world", plainData.String()) 351 | }) 352 | 353 | t.Run("With invalid network", func(tt *testing.T) { 354 | network, err := fixed.FromInfo(`{"public_key":"07e1d1d335df83fa98462005690372c643340060d205306a9aa8106b6bd0b3820557ec32c2ad488e4d4f6008f89a346f18492092ccc0d594610de2732c8b808f0095685ae3a85ba243747b1b2f426049010f6b73a0cf1d389351d5aaaa1047f6297d3a4f9749b33eb2d904c9d9ebf17224150ddd7abd7567a9bec6c74480ee0b","period":3,"genesis_time":1727521075,"genesis_seed":"cd7ad2f0e0cce5d8c288f2dd016ffe7bc8dc88dbb229b3da2b6ad736490dfed6","chain_hash":"04f1e9062b8a81f848fded9c12306733282b2727ecced50032187751166ec8c3","scheme":"bls-bn254-unchained-on-g1","beacon_id":"evmnet"}`) 355 | require.NoError(tt, err) 356 | 357 | testReader := strings.NewReader(cipher) 358 | var plainData bytes.Buffer 359 | 360 | err = tlock.New(network).Strict().Decrypt(&plainData, testReader) 361 | require.ErrorIs(tt, err, tlock.ErrWrongChainhash) 362 | }) 363 | 364 | t.Run("With quicknet-t invalid network", func(tt *testing.T) { 365 | network, err := fixed.FromInfo(`{"public_key":"b15b65b46fb29104f6a4b5d1e11a8da6344463973d423661bb0804846a0ecd1ef93c25057f1c0baab2ac53e56c662b66072f6d84ee791a3382bfb055afab1e6a375538d8ffc451104ac971d2dc9b168e2d3246b0be2015969cbaac298f6502da","period":3,"genesis_time":1689232296,"genesis_seed":"40d49d910472d4adb1d67f65db8332f11b4284eecf05c05c5eacd5eef7d40e2d","chain_hash":"cc9c398442737cbd141526600919edd69f1d6f9b4adb67e4d912fbc64341a9a5","scheme":"bls-unchained-g1-rfc9380","beacon_id":"quicknet-t"}`) 366 | require.NoError(tt, err) 367 | 368 | testReader := strings.NewReader(cipher) 369 | var plainData bytes.Buffer 370 | 371 | err = tlock.New(network).Strict().Decrypt(&plainData, testReader) 372 | require.ErrorIs(tt, err, tlock.ErrWrongChainhash) 373 | }) 374 | } 375 | 376 | func TestInteropWithJS(t *testing.T) { 377 | t.Run("on Mainnet with G1 sigs", func(t *testing.T) { 378 | cipher := `-----BEGIN AGE ENCRYPTED FILE----- 379 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDEyMDQxMTI1IDUyZGI5YmE3 380 | MGUwY2MwZjZlYWY3ODAzZGQwNzQ0N2ExZjU0Nzc3MzVmZDNmNjYxNzkyYmE5NDYw 381 | MGM4NGU5NzEKbDNtWFdseFRIS0YxQi9HZGYyMzJ0cmkveDFWZk5zVDMwS002eExV 382 | NXUwbFFqQVdNSFJmVHJYbnFJOWpHWWM4ZApETmVodVhaUm8zay9HVzVMVDNaN1M1 383 | d3JVN0lvQVNQUy9xY3JjODNIWEplY25wTXVJS1ZTM3Fyc0NvZzJiZW1OCjVJQmRD 384 | VDU4UUZGeVJ5QzRlRUFZU092NWl0b3E2UWw1RDh6WEtVdmdTTFkKLS0tIEk5c0th 385 | Mi9yeEF2ZDFlL1paTFlIV2VZYkVZVjlreDFidE1wWm1rMU51QkUKxCgEsEjSEixh 386 | 4nEBtpolrubLO6WwhfWuh5ZFewjuXbSyrJGreivurDm+7y5stuDO6xPVRpcU+eSQ 387 | RLrz 388 | -----END AGE ENCRYPTED FILE-----` 389 | expected := "hello world and other things" 390 | network, err := http.NewNetwork(mainnetHost, mainnetQuicknet) 391 | require.NoError(t, err) 392 | 393 | testReader := strings.NewReader(cipher) 394 | var plainData bytes.Buffer 395 | 396 | err = tlock.New(network).Decrypt(&plainData, testReader) 397 | require.NoError(t, err) 398 | 399 | require.Equal(t, expected, plainData.String()) 400 | }) 401 | 402 | t.Run("on testnet with quicknet-t", func(t *testing.T) { 403 | cipher := `-----BEGIN AGE ENCRYPTED FILE----- 404 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRsb2NrIDE2MjQ5MTAgY2M5YzM5ODQ0 405 | MjczN2NiZDE0MTUyNjYwMDkxOWVkZDY5ZjFkNmY5YjRhZGI2N2U0ZDkxMmZiYzY0 406 | MzQxYTlhNQpqTTVLOEhWVUFrOFFkNStIL0ZQOHplRkZPSEs4T0pjVG1FNW9LSW1z 407 | bytQRmRDM3lycEdtRGFtck9XMGVycDcxCkVuS1hqL216dmI3RThFMDZMWTNWZEh5 408 | SWh3UFhWWFJlREZ5SHZiTWNPMDdNcWFLamV5MWRNMkMwTHR1SjNpWUoKeENEaEJQ 409 | RDF3K3JjbEtNenI3QU5VVldWa3FmMHd0aGtxTmw3VEEwK0RjQQotLS0gUWFpL0U5 410 | VDNsVkpZT3F2Mk14NWRIU3IzbnhuUUsyaTdsS0ptclNoNk9lOAqkjk0Ypkj6JxKk 411 | 5ZxeTXAsxRyy9yptL4yKgd2i/J7k/O3C0Te7yPwsdkUC 412 | -----END AGE ENCRYPTED FILE-----` 413 | expected := "test today\n" 414 | network, err := http.NewNetwork(testnetHost, testnetQuicknetT) 415 | require.NoError(t, err) 416 | 417 | testReader := strings.NewReader(cipher) 418 | var plainData bytes.Buffer 419 | 420 | err = tlock.New(network).Decrypt(&plainData, testReader) 421 | require.NoError(t, err) 422 | 423 | require.Equal(t, expected, plainData.String()) 424 | }) 425 | 426 | } 427 | --------------------------------------------------------------------------------