├── .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 |
--------------------------------------------------------------------------------