TimeRelease 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Leo Pagano
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.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include LICENSE.txt
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: TimeRelease
3
+ Version: 1.0.0
4
+ Summary: Python utility to cryptographically lock secrets for a preset amount of time.
5
+ Author-email: Leo Pagano <leo@leoapagano.com>
6
+ Maintainer-email: Leo Pagano <leo@leoapagano.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 Leo Pagano
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Project-URL: Homepage, https://github.com/leoapagano/TimeRelease
29
+ Project-URL: Documentation, https://github.com/leoapagano/TimeRelease/blob/main/README.md
30
+ Keywords: TimeRelease,Time,Release,Encryption,Crypto,Cryptography,Pagano
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Programming Language :: Python :: Implementation :: CPython
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: Operating System :: POSIX :: Linux
36
+ Classifier: Operating System :: Microsoft :: Windows
37
+ Classifier: Operating System :: MacOS
38
+ Classifier: Operating System :: MacOS :: MacOS X
39
+ Requires-Python: >=3.13
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Requires-Dist: mpmath~=1.0
43
+ Requires-Dist: pycryptodome~=3.0
44
+ Requires-Dist: sympy~=1.0
45
+ Requires-Dist: tqdm~=4.0
46
+ Provides-Extra: dev
47
+ Requires-Dist: build==1.4.0; extra == "dev"
48
+ Requires-Dist: pre-commit==4.5.1; extra == "dev"
49
+ Requires-Dist: pytest==9.0.2; extra == "dev"
50
+ Requires-Dist: ruff==0.15.5; extra == "dev"
51
+ Requires-Dist: twine==6.2.0; extra == "dev"
52
+ Requires-Dist: ty==0.0.21; extra == "dev"
53
+ Dynamic: license-file
54
+
55
+ # TimeRelease
56
+
57
+ ## Introduction
58
+
59
+ A simple Python utility to lock (or encrypt) secrets, such that they require a preset amount of compute to unlock (or decrypt) them later.
60
+
61
+ Since each cryptographic step requires the last, it (by design) cannot be parallelized.
62
+
63
+ This could potentially be useful if you want to wait, for example, an hour, before being able to access a password; as a form of stolen device protection.
64
+
65
+ ## WARNING!
66
+
67
+ Always keep your "encrypted" file secret, treating it as if it were a password. It is effectively unencrypted as it can be used to decrypt the secret (if such an attacker has sufficient time to complete the puzzle) without any other knowledge of the secret or the individual who encrypted it.
68
+
69
+ ## Table of Contents
70
+
71
+ - [Interactive Usage](#interactive-usage)
72
+ - [Setup](#setup)
73
+ - [Encryption](#encryption)
74
+ - [Decryption](#decryption)
75
+ - [Module API](#module-api)
76
+ - [Development Usage](#development-usage)
77
+
78
+ ---
79
+
80
+ ## Interactive Usage
81
+
82
+ ### Setup
83
+
84
+ Install the package and its dependencies in a venv:
85
+
86
+ ```bash
87
+ python3 -m venv ./venv
88
+ source ./venv/bin/activate
89
+ pip install TimeRelease
90
+ ```
91
+
92
+ ### Encryption
93
+
94
+ To encrypt a file `somefile.txt` -> `somefile.txt.timerelease`:
95
+
96
+ ```bash
97
+ # Specify # of seconds it should take to unlock on your machine
98
+ timerelease --encrypt somefile.txt somefile.txt.timerelease --time 3600
99
+ # Or, specify raw # of iterations needed to unlock the file
100
+ timerelease --encrypt somefile.txt somefile.txt.timerelease --iters 1073741824
101
+ ```
102
+
103
+ You can use this to determine how long (on your hardware) the decryption process will take. Encryption should be nearly instant for most settings, even for comically large numbers of iterations.
104
+
105
+ ### Decryption
106
+
107
+ To decrypt a file `somefile.txt.timerelease` -> `somefile.txt`:
108
+
109
+ ```bash
110
+ timerelease --decrypt somefile.txt.timerelease somefile.txt
111
+ ```
112
+
113
+ It will take however long the person who encrypted the file dictates. The only way to speed it up is to find a CPU with really, really fast single-core performance.
114
+
115
+ ## Module API
116
+
117
+ Like any module, TimeRelease can be installed via pip (`pip install TimeRelease`) and imported (`import TimeRelease`). The API for the TimeRelease module is as follows:
118
+
119
+ #### `TimeRelease.decrypt_secret(enc_package)`
120
+
121
+ * Given a dictionary which was generated by encrypt_secret(), decrypt_secret() decrypts it and returns the secret as a byte string.
122
+ * If `logging` (defaults to True) is set to True, a progress bar is printed, along with extra debug information.
123
+
124
+ #### `TimeRelease.encrypt_secret()`
125
+
126
+ * Given a byte string `secret`, encrypt_secret() applies `iterations` iterations to it, "encrypting" it.
127
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
128
+
129
+ #### `TimeRelease.run_single_benchmark()`
130
+
131
+ * Determines how long it takes for the CPU to run a fixed number of `iterations`.
132
+ * Amount of time elapsed is returned in seconds as a float.
133
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
134
+
135
+ #### `TimeRelease.run_benchmark()`
136
+
137
+ * Determines how many iterations a single core of the user's CPU is capable of processing, on average, per second (as an integer).
138
+ * `benches` (defaults to 10) describes the number of times the benchmark will be run.
139
+ * Higher values of `benches` = greater certainty in running time, but more time is spent benchmarking.
140
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
141
+
142
+ ## Development Usage
143
+
144
+ To install development versions of this package, clone this repo and `cd` into it, then run:
145
+
146
+ ```bash
147
+ python3 -m venv ./venv
148
+ source ./venv/bin/activate
149
+ pip install -e .[dev]
150
+ pre-commit install
151
+ ```
152
+
153
+ To test the codebase, simply run `pytest`. To apply proper formatting to the codebase, run `ruff format .`. To ensure that the code has no other issues with PEP8 compliance, run `ruff check`. And to ensure that there are no type errors, please run `ty check TimeRelease`. Please do all of these things before you commit, and not after!
154
+
155
+ To build a distribution package, first update the version number in `pyproject.toml`, and make a tag for that release version at the current commit. Then:
156
+
157
+ ```bash
158
+ python3 -m build
159
+ ```
160
+
161
+ To upload this build to PyPI, run:
162
+
163
+ ```bash
164
+ python3 -m twine upload dist/* --skip-existing
165
+ ```
@@ -0,0 +1,111 @@
1
+ # TimeRelease
2
+
3
+ ## Introduction
4
+
5
+ A simple Python utility to lock (or encrypt) secrets, such that they require a preset amount of compute to unlock (or decrypt) them later.
6
+
7
+ Since each cryptographic step requires the last, it (by design) cannot be parallelized.
8
+
9
+ This could potentially be useful if you want to wait, for example, an hour, before being able to access a password; as a form of stolen device protection.
10
+
11
+ ## WARNING!
12
+
13
+ Always keep your "encrypted" file secret, treating it as if it were a password. It is effectively unencrypted as it can be used to decrypt the secret (if such an attacker has sufficient time to complete the puzzle) without any other knowledge of the secret or the individual who encrypted it.
14
+
15
+ ## Table of Contents
16
+
17
+ - [Interactive Usage](#interactive-usage)
18
+ - [Setup](#setup)
19
+ - [Encryption](#encryption)
20
+ - [Decryption](#decryption)
21
+ - [Module API](#module-api)
22
+ - [Development Usage](#development-usage)
23
+
24
+ ---
25
+
26
+ ## Interactive Usage
27
+
28
+ ### Setup
29
+
30
+ Install the package and its dependencies in a venv:
31
+
32
+ ```bash
33
+ python3 -m venv ./venv
34
+ source ./venv/bin/activate
35
+ pip install TimeRelease
36
+ ```
37
+
38
+ ### Encryption
39
+
40
+ To encrypt a file `somefile.txt` -> `somefile.txt.timerelease`:
41
+
42
+ ```bash
43
+ # Specify # of seconds it should take to unlock on your machine
44
+ timerelease --encrypt somefile.txt somefile.txt.timerelease --time 3600
45
+ # Or, specify raw # of iterations needed to unlock the file
46
+ timerelease --encrypt somefile.txt somefile.txt.timerelease --iters 1073741824
47
+ ```
48
+
49
+ You can use this to determine how long (on your hardware) the decryption process will take. Encryption should be nearly instant for most settings, even for comically large numbers of iterations.
50
+
51
+ ### Decryption
52
+
53
+ To decrypt a file `somefile.txt.timerelease` -> `somefile.txt`:
54
+
55
+ ```bash
56
+ timerelease --decrypt somefile.txt.timerelease somefile.txt
57
+ ```
58
+
59
+ It will take however long the person who encrypted the file dictates. The only way to speed it up is to find a CPU with really, really fast single-core performance.
60
+
61
+ ## Module API
62
+
63
+ Like any module, TimeRelease can be installed via pip (`pip install TimeRelease`) and imported (`import TimeRelease`). The API for the TimeRelease module is as follows:
64
+
65
+ #### `TimeRelease.decrypt_secret(enc_package)`
66
+
67
+ * Given a dictionary which was generated by encrypt_secret(), decrypt_secret() decrypts it and returns the secret as a byte string.
68
+ * If `logging` (defaults to True) is set to True, a progress bar is printed, along with extra debug information.
69
+
70
+ #### `TimeRelease.encrypt_secret()`
71
+
72
+ * Given a byte string `secret`, encrypt_secret() applies `iterations` iterations to it, "encrypting" it.
73
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
74
+
75
+ #### `TimeRelease.run_single_benchmark()`
76
+
77
+ * Determines how long it takes for the CPU to run a fixed number of `iterations`.
78
+ * Amount of time elapsed is returned in seconds as a float.
79
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
80
+
81
+ #### `TimeRelease.run_benchmark()`
82
+
83
+ * Determines how many iterations a single core of the user's CPU is capable of processing, on average, per second (as an integer).
84
+ * `benches` (defaults to 10) describes the number of times the benchmark will be run.
85
+ * Higher values of `benches` = greater certainty in running time, but more time is spent benchmarking.
86
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
87
+
88
+ ## Development Usage
89
+
90
+ To install development versions of this package, clone this repo and `cd` into it, then run:
91
+
92
+ ```bash
93
+ python3 -m venv ./venv
94
+ source ./venv/bin/activate
95
+ pip install -e .[dev]
96
+ pre-commit install
97
+ ```
98
+
99
+ To test the codebase, simply run `pytest`. To apply proper formatting to the codebase, run `ruff format .`. To ensure that the code has no other issues with PEP8 compliance, run `ruff check`. And to ensure that there are no type errors, please run `ty check TimeRelease`. Please do all of these things before you commit, and not after!
100
+
101
+ To build a distribution package, first update the version number in `pyproject.toml`, and make a tag for that release version at the current commit. Then:
102
+
103
+ ```bash
104
+ python3 -m build
105
+ ```
106
+
107
+ To upload this build to PyPI, run:
108
+
109
+ ```bash
110
+ python3 -m twine upload dist/* --skip-existing
111
+ ```
@@ -0,0 +1,5 @@
1
+ from .bench import run_benchmark, run_single_benchmark
2
+ from .decrypt import decrypt_secret
3
+ from .encrypt import encrypt_secret
4
+
5
+ __all__ = ["decrypt_secret", "encrypt_secret", "run_single_benchmark", "run_benchmark"]
@@ -0,0 +1,104 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from .bench import run_benchmark
6
+ from .decrypt import decrypt_secret
7
+ from .encrypt import encrypt_secret
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> int | None:
11
+ # Read arguments
12
+ parser = argparse.ArgumentParser(prog="TimeRelease")
13
+ group = parser.add_mutually_exclusive_group(required=True)
14
+ group.add_argument(
15
+ "--encrypt",
16
+ nargs=2,
17
+ metavar=("INFILE", "OUTFILE"),
18
+ help="Encrypt input file to output file path",
19
+ )
20
+ group.add_argument(
21
+ "--decrypt",
22
+ nargs=2,
23
+ metavar=("INFILE", "OUTFILE"),
24
+ help="Decrypt input file to output file path",
25
+ )
26
+ iters_group = parser.add_mutually_exclusive_group()
27
+ iters_group.add_argument(
28
+ "--iters",
29
+ type=int,
30
+ metavar="N",
31
+ help="Number of iterations for encryption (required with --encrypt)",
32
+ )
33
+ iters_group.add_argument(
34
+ "--time",
35
+ type=float,
36
+ metavar="SECONDS",
37
+ help="Target decryption time in seconds; benchmarks your CPU to compute iterations (required with --encrypt)",
38
+ )
39
+ args = parser.parse_args(argv)
40
+
41
+ # Validate --iters/--time usage
42
+ if args.encrypt and args.iters is None and args.time is None:
43
+ parser.error("--encrypt requires either --iters or --time")
44
+ if (args.iters is not None or args.time is not None) and not args.encrypt:
45
+ parser.error("--iters and --time can only be used with --encrypt")
46
+
47
+ # Determine and validate infile and outfile
48
+ if args.encrypt:
49
+ infile, outfile = Path(args.encrypt[0]), Path(args.encrypt[1])
50
+ elif args.decrypt:
51
+ infile, outfile = Path(args.decrypt[0]), Path(args.decrypt[1])
52
+ if not infile.is_file():
53
+ print(f"{infile} does not exist or is not a file")
54
+ return -1
55
+ if outfile.is_file():
56
+ print(f"{outfile} will be overwritten! Proceed anyway? [y/n]")
57
+ if input(">>> ").strip().lower() != "y":
58
+ print("Exiting.")
59
+ return -1
60
+ if outfile.is_dir():
61
+ print(f"{outfile} cannot be overwritten: is a directory")
62
+ return -1
63
+
64
+ # ENCRYPTION
65
+ if args.encrypt:
66
+ # Determine # of iterations
67
+ if args.iters is not None:
68
+ iters = args.iters
69
+ else:
70
+ print("TimeRelease is benchmarking your CPU. This may take up to a minute.")
71
+ ips = run_benchmark(logging=False)
72
+ iters = int(args.time * ips)
73
+
74
+ print(f"Encrypting {infile} -> {outfile}:")
75
+
76
+ # Read infile
77
+ with open(infile, "rb") as f:
78
+ secret = f.read()
79
+
80
+ # Encrypt secret & get JSON package
81
+ enc_package = encrypt_secret(secret, iters)
82
+
83
+ # Write outfile
84
+ with open(outfile, "w") as f:
85
+ json.dump(enc_package, f, indent=4)
86
+
87
+ # DECRYPTION
88
+ elif args.decrypt:
89
+ print(f"Decrypting {infile} -> {outfile}:")
90
+
91
+ # Read infile
92
+ with open(infile, "r") as f:
93
+ enc_package = json.load(f)
94
+
95
+ # Decrypt
96
+ decrypted = decrypt_secret(enc_package)
97
+
98
+ # Write outfile
99
+ with open(outfile, "wb") as f:
100
+ f.write(decrypted)
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
@@ -0,0 +1,11 @@
1
+ import base64
2
+
3
+
4
+ def base64_to_byte_str(base64_string: str) -> bytes:
5
+ """Converts a base64 string to its byte string representation."""
6
+ return base64.b64decode(base64_string.encode("ASCII"))
7
+
8
+
9
+ def byte_str_to_base64(byte_string: bytes | bytearray | memoryview) -> str:
10
+ """Converts a byte string to its base64 (non-byte string) representation."""
11
+ return base64.b64encode(byte_string).decode("ASCII")
@@ -0,0 +1,57 @@
1
+ import random
2
+ import time
3
+
4
+ from sympy import nextprime
5
+
6
+
7
+ def run_single_benchmark(iterations: int, logging: bool = True) -> float:
8
+ """
9
+ Determines how long it takes for the CPU to run a fixed number of `iterations`.
10
+ Amount of time elapsed is returned in seconds as a float.
11
+ If `logging` (defaults to True) is set to True, extra debug information is printed.
12
+ """
13
+ # Setup dummy puzzle parameters
14
+ if logging:
15
+ print("Preparing time lock...")
16
+ p = nextprime(random.getrandbits(512))
17
+ q = nextprime(random.getrandbits(512))
18
+ modulus = p * q
19
+
20
+ # Execute & time it
21
+ if logging:
22
+ print("Running benchmark...")
23
+ start_time = time.time()
24
+ r = random.randint(2, modulus - 1)
25
+ for _ in range(iterations):
26
+ r = pow(r, 2, modulus)
27
+ end_time = time.time()
28
+
29
+ return end_time - start_time
30
+
31
+
32
+ def run_benchmark(benches: int = 10, logging: bool = True) -> int:
33
+ """
34
+ Determines how many iterations a single core of the user's CPU is capable of processing, on average, per second (as an integer).
35
+ `benches` (defaults to 10) describes the number of times the benchmark will be run.
36
+ Higher values of `benches` = greater certainty in running time, but more time is spent benchmarking.
37
+ If `logging` (defaults to True) is set to True, extra debug information is printed.
38
+ """
39
+ # Repeat single benchmarks, doubling iterations until the time per benchmark is less than 1 second
40
+ iterations = 1
41
+ execution_time = 0
42
+ while execution_time < 1:
43
+ iterations *= 2
44
+ if logging:
45
+ print(f"Running benchmark @ iterations={iterations}")
46
+ execution_time = run_single_benchmark(iterations, logging=logging)
47
+
48
+ # Run benches (default: 10) benchmarks with this number of iterations
49
+ results = []
50
+ for i in range(benches):
51
+ if logging:
52
+ print(f"Running standard benchmark {i + 1} / {benches}")
53
+ results.append(run_single_benchmark(iterations, logging=logging))
54
+
55
+ # Determine average iterations/sec and return
56
+ avg = sum(results) / benches
57
+ return int(iterations // avg)
@@ -0,0 +1,56 @@
1
+ import hashlib
2
+ import time
3
+ from typing import Any
4
+
5
+ from Crypto.Cipher import AES
6
+ from Crypto.Util.Padding import unpad
7
+ from tqdm import tqdm
8
+
9
+ from .b64utils import base64_to_byte_str
10
+
11
+
12
+ def decrypt_secret(enc_package: dict[str, Any], logging: bool = True) -> bytes:
13
+ """
14
+ Given a dictionary which was generated by encrypt_secret(), decrypt_secret() decrypts it and returns the secret as a byte string.
15
+ If `logging` (defaults to True) is set to True, a progress bar is printed, along with extra debug information.
16
+ """
17
+ base = enc_package["base"]
18
+ modulus = enc_package["modulus"]
19
+ iterations = enc_package["iterations"]
20
+ secret_iv = base64_to_byte_str(enc_package["secret_iv"])
21
+ encrypted_secret = base64_to_byte_str(enc_package["encrypted_secret"])
22
+ key_iv = base64_to_byte_str(enc_package["key_iv"])
23
+ encrypted_key = base64_to_byte_str(enc_package["encrypted_key"])
24
+
25
+ # Compute puzzle result r
26
+ if logging:
27
+ print("Unlocking encryption key...")
28
+ start_time = time.time()
29
+ r = base
30
+ if logging:
31
+ for _ in tqdm(range(iterations), desc="Unlocking", unit="iters"):
32
+ r = pow(r, 2, modulus)
33
+ else:
34
+ for _ in range(iterations):
35
+ r = pow(r, 2, modulus)
36
+ end_time = time.time()
37
+ if logging:
38
+ print(f"Completed in {end_time - start_time:.2f} seconds")
39
+
40
+ # Derive AES key from r
41
+ if logging:
42
+ print("Deriving AES key...")
43
+ derived_key = hashlib.sha256(str(r).encode()).digest()[:16]
44
+
45
+ # Decrypt AES key
46
+ if logging:
47
+ print("Decrypting AES key...")
48
+ cipher2 = AES.new(derived_key, AES.MODE_CBC, iv=key_iv)
49
+ aes_key = unpad(cipher2.decrypt(encrypted_key), AES.block_size)
50
+
51
+ # Decrypt secret
52
+ if logging:
53
+ print("Decrypting secret...")
54
+ cipher = AES.new(aes_key, AES.MODE_CBC, iv=secret_iv)
55
+ secret = unpad(cipher.decrypt(encrypted_secret), AES.block_size)
56
+ return secret
@@ -0,0 +1,61 @@
1
+ import hashlib
2
+ import random
3
+ from typing import Any
4
+
5
+ from Crypto.Cipher import AES
6
+ from Crypto.Random import get_random_bytes
7
+ from Crypto.Util.Padding import pad
8
+ from sympy import nextprime
9
+
10
+ from .b64utils import byte_str_to_base64
11
+
12
+
13
+ def encrypt_secret(
14
+ secret: bytes, iterations: int, logging: bool = True
15
+ ) -> dict[str, Any]:
16
+ """
17
+ Given a byte string `secret`, encrypt_secret() applies `iterations` iterations to it, "encrypting" it.
18
+ If `logging` (defaults to True) is set to True, extra debug information is printed.
19
+ """
20
+ # Create new (symmetric) AES key and encrypt secret with it
21
+ if logging:
22
+ print("Generating encryption key...")
23
+ aes_key = get_random_bytes(16)
24
+ cipher = AES.new(aes_key, AES.MODE_CBC)
25
+ secret_iv = cipher.iv
26
+ encrypted_secret = cipher.encrypt(pad(secret, AES.block_size))
27
+
28
+ # Time-lock puzzle setup
29
+ # Generate N = p * q
30
+ if logging:
31
+ print("Preparing time lock...")
32
+ p = nextprime(random.getrandbits(512))
33
+ q = nextprime(random.getrandbits(512))
34
+ modulus = p * q
35
+
36
+ # Puzzle: compute r = base^(2^iterations) mod modulus
37
+ base = random.randint(2, modulus - 1)
38
+ r = pow(base, pow(2, iterations, (p - 1) * (q - 1)), modulus)
39
+
40
+ # Derive AES key from r (e.g. using SHA256)
41
+ if logging:
42
+ print("Deriving AES key from time lock...")
43
+ derived_key = hashlib.sha256(str(r).encode()).digest()[:16]
44
+
45
+ # Encrypt AES key with derived key
46
+ if logging:
47
+ print("Encrypting derived key...")
48
+ cipher2 = AES.new(derived_key, AES.MODE_CBC)
49
+ key_iv = cipher2.iv
50
+ encrypted_key = cipher2.encrypt(pad(aes_key, AES.block_size))
51
+
52
+ # Stash stuff needed to decrypt later
53
+ return {
54
+ "base": base,
55
+ "modulus": modulus,
56
+ "iterations": iterations,
57
+ "secret_iv": byte_str_to_base64(secret_iv),
58
+ "encrypted_secret": byte_str_to_base64(encrypted_secret),
59
+ "key_iv": byte_str_to_base64(key_iv),
60
+ "encrypted_key": byte_str_to_base64(encrypted_key),
61
+ }
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: TimeRelease
3
+ Version: 1.0.0
4
+ Summary: Python utility to cryptographically lock secrets for a preset amount of time.
5
+ Author-email: Leo Pagano <leo@leoapagano.com>
6
+ Maintainer-email: Leo Pagano <leo@leoapagano.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 Leo Pagano
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Project-URL: Homepage, https://github.com/leoapagano/TimeRelease
29
+ Project-URL: Documentation, https://github.com/leoapagano/TimeRelease/blob/main/README.md
30
+ Keywords: TimeRelease,Time,Release,Encryption,Crypto,Cryptography,Pagano
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Programming Language :: Python :: Implementation :: CPython
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: Operating System :: POSIX :: Linux
36
+ Classifier: Operating System :: Microsoft :: Windows
37
+ Classifier: Operating System :: MacOS
38
+ Classifier: Operating System :: MacOS :: MacOS X
39
+ Requires-Python: >=3.13
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Requires-Dist: mpmath~=1.0
43
+ Requires-Dist: pycryptodome~=3.0
44
+ Requires-Dist: sympy~=1.0
45
+ Requires-Dist: tqdm~=4.0
46
+ Provides-Extra: dev
47
+ Requires-Dist: build==1.4.0; extra == "dev"
48
+ Requires-Dist: pre-commit==4.5.1; extra == "dev"
49
+ Requires-Dist: pytest==9.0.2; extra == "dev"
50
+ Requires-Dist: ruff==0.15.5; extra == "dev"
51
+ Requires-Dist: twine==6.2.0; extra == "dev"
52
+ Requires-Dist: ty==0.0.21; extra == "dev"
53
+ Dynamic: license-file
54
+
55
+ # TimeRelease
56
+
57
+ ## Introduction
58
+
59
+ A simple Python utility to lock (or encrypt) secrets, such that they require a preset amount of compute to unlock (or decrypt) them later.
60
+
61
+ Since each cryptographic step requires the last, it (by design) cannot be parallelized.
62
+
63
+ This could potentially be useful if you want to wait, for example, an hour, before being able to access a password; as a form of stolen device protection.
64
+
65
+ ## WARNING!
66
+
67
+ Always keep your "encrypted" file secret, treating it as if it were a password. It is effectively unencrypted as it can be used to decrypt the secret (if such an attacker has sufficient time to complete the puzzle) without any other knowledge of the secret or the individual who encrypted it.
68
+
69
+ ## Table of Contents
70
+
71
+ - [Interactive Usage](#interactive-usage)
72
+ - [Setup](#setup)
73
+ - [Encryption](#encryption)
74
+ - [Decryption](#decryption)
75
+ - [Module API](#module-api)
76
+ - [Development Usage](#development-usage)
77
+
78
+ ---
79
+
80
+ ## Interactive Usage
81
+
82
+ ### Setup
83
+
84
+ Install the package and its dependencies in a venv:
85
+
86
+ ```bash
87
+ python3 -m venv ./venv
88
+ source ./venv/bin/activate
89
+ pip install TimeRelease
90
+ ```
91
+
92
+ ### Encryption
93
+
94
+ To encrypt a file `somefile.txt` -> `somefile.txt.timerelease`:
95
+
96
+ ```bash
97
+ # Specify # of seconds it should take to unlock on your machine
98
+ timerelease --encrypt somefile.txt somefile.txt.timerelease --time 3600
99
+ # Or, specify raw # of iterations needed to unlock the file
100
+ timerelease --encrypt somefile.txt somefile.txt.timerelease --iters 1073741824
101
+ ```
102
+
103
+ You can use this to determine how long (on your hardware) the decryption process will take. Encryption should be nearly instant for most settings, even for comically large numbers of iterations.
104
+
105
+ ### Decryption
106
+
107
+ To decrypt a file `somefile.txt.timerelease` -> `somefile.txt`:
108
+
109
+ ```bash
110
+ timerelease --decrypt somefile.txt.timerelease somefile.txt
111
+ ```
112
+
113
+ It will take however long the person who encrypted the file dictates. The only way to speed it up is to find a CPU with really, really fast single-core performance.
114
+
115
+ ## Module API
116
+
117
+ Like any module, TimeRelease can be installed via pip (`pip install TimeRelease`) and imported (`import TimeRelease`). The API for the TimeRelease module is as follows:
118
+
119
+ #### `TimeRelease.decrypt_secret(enc_package)`
120
+
121
+ * Given a dictionary which was generated by encrypt_secret(), decrypt_secret() decrypts it and returns the secret as a byte string.
122
+ * If `logging` (defaults to True) is set to True, a progress bar is printed, along with extra debug information.
123
+
124
+ #### `TimeRelease.encrypt_secret()`
125
+
126
+ * Given a byte string `secret`, encrypt_secret() applies `iterations` iterations to it, "encrypting" it.
127
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
128
+
129
+ #### `TimeRelease.run_single_benchmark()`
130
+
131
+ * Determines how long it takes for the CPU to run a fixed number of `iterations`.
132
+ * Amount of time elapsed is returned in seconds as a float.
133
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
134
+
135
+ #### `TimeRelease.run_benchmark()`
136
+
137
+ * Determines how many iterations a single core of the user's CPU is capable of processing, on average, per second (as an integer).
138
+ * `benches` (defaults to 10) describes the number of times the benchmark will be run.
139
+ * Higher values of `benches` = greater certainty in running time, but more time is spent benchmarking.
140
+ * If `logging` (defaults to True) is set to True, extra debug information is printed.
141
+
142
+ ## Development Usage
143
+
144
+ To install development versions of this package, clone this repo and `cd` into it, then run:
145
+
146
+ ```bash
147
+ python3 -m venv ./venv
148
+ source ./venv/bin/activate
149
+ pip install -e .[dev]
150
+ pre-commit install
151
+ ```
152
+
153
+ To test the codebase, simply run `pytest`. To apply proper formatting to the codebase, run `ruff format .`. To ensure that the code has no other issues with PEP8 compliance, run `ruff check`. And to ensure that there are no type errors, please run `ty check TimeRelease`. Please do all of these things before you commit, and not after!
154
+
155
+ To build a distribution package, first update the version number in `pyproject.toml`, and make a tag for that release version at the current commit. Then:
156
+
157
+ ```bash
158
+ python3 -m build
159
+ ```
160
+
161
+ To upload this build to PyPI, run:
162
+
163
+ ```bash
164
+ python3 -m twine upload dist/* --skip-existing
165
+ ```
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ TimeRelease/__init__.py
6
+ TimeRelease/__main__.py
7
+ TimeRelease/b64utils.py
8
+ TimeRelease/bench.py
9
+ TimeRelease/decrypt.py
10
+ TimeRelease/encrypt.py
11
+ TimeRelease.egg-info/PKG-INFO
12
+ TimeRelease.egg-info/SOURCES.txt
13
+ TimeRelease.egg-info/dependency_links.txt
14
+ TimeRelease.egg-info/entry_points.txt
15
+ TimeRelease.egg-info/requires.txt
16
+ TimeRelease.egg-info/top_level.txt
17
+ tests/test_encrypt_decrypt.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ timerelease = TimeRelease.__main__:main
@@ -0,0 +1,12 @@
1
+ mpmath~=1.0
2
+ pycryptodome~=3.0
3
+ sympy~=1.0
4
+ tqdm~=4.0
5
+
6
+ [dev]
7
+ build==1.4.0
8
+ pre-commit==4.5.1
9
+ pytest==9.0.2
10
+ ruff==0.15.5
11
+ twine==6.2.0
12
+ ty==0.0.21
@@ -0,0 +1 @@
1
+ TimeRelease
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "TimeRelease"
7
+ version = "1.0.0"
8
+ requires-python = ">=3.13"
9
+ description = "Python utility to cryptographically lock secrets for a preset amount of time."
10
+ readme = "README.md"
11
+ authors = [
12
+ { name = "Leo Pagano", email = "leo@leoapagano.com" }
13
+ ]
14
+ maintainers = [
15
+ { name = "Leo Pagano", email = "leo@leoapagano.com" }
16
+ ]
17
+ license = { file = "LICENSE" }
18
+ keywords = [
19
+ "TimeRelease",
20
+ "Time",
21
+ "Release",
22
+ "Encryption",
23
+ "Crypto",
24
+ "Cryptography",
25
+ "Pagano"
26
+ ]
27
+ classifiers = [
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: Implementation :: CPython",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Intended Audience :: Developers",
32
+ "Operating System :: POSIX :: Linux",
33
+ "Operating System :: Microsoft :: Windows",
34
+ "Operating System :: MacOS",
35
+ "Operating System :: MacOS :: MacOS X",
36
+ ]
37
+ dependencies = [
38
+ "mpmath~=1.0",
39
+ "pycryptodome~=3.0",
40
+ "sympy~=1.0",
41
+ "tqdm~=4.0"
42
+ ]
43
+
44
+ [project.optional-dependencies]
45
+ dev = [
46
+ "build==1.4.0",
47
+ "pre-commit==4.5.1",
48
+ "pytest==9.0.2",
49
+ "ruff==0.15.5",
50
+ "twine==6.2.0",
51
+ "ty==0.0.21"
52
+ ]
53
+
54
+ [project.urls]
55
+ Homepage = "https://github.com/leoapagano/TimeRelease"
56
+ Documentation = "https://github.com/leoapagano/TimeRelease/blob/main/README.md"
57
+
58
+ [project.scripts]
59
+ timerelease = "TimeRelease.__main__:main"
60
+
61
+ [tool.ruff.format]
62
+ indent-style = "tab"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ from TimeRelease import decrypt_secret, encrypt_secret
2
+
3
+
4
+ def test_hello_world():
5
+ """
6
+ Very basic test case. Just ensures that an input, when encrypted and decrypted,
7
+ produces the same output that it started with.
8
+ """
9
+ input = b"hello world"
10
+ encrypted = encrypt_secret(input, 2**16, logging=False)
11
+ decrypted = decrypt_secret(encrypted, logging=False)
12
+ assert decrypted == b"hello world"