utilosafe 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
+ Metadata-Version: 2.4
2
+ Name: utilosafe
3
+ Version: 1.0.0
4
+ Author-email: Helmut Konrad Schewe <helmutus@outlook.com>
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/anaticulae/utilosafe
7
+ Project-URL: Repository, https://github.com/anaticulae/utilosafe
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: utilo<3.0.0,>=2.107.3
14
+ Requires-Dist: configos<3.0.0,>=1.0.4
15
+ Requires-Dist: cryptography<39.0.0,>=38.0.3
16
+ Requires-Dist: bcrypt<5.0.0,>=4.0.1
17
+ Provides-Extra: dev
18
+ Requires-Dist: utilotest<1.0.0,>=0.26.0; extra == "dev"
19
+ Requires-Dist: power<3.0.0,==2.20.1; extra == "dev"
20
+
21
+ # utilosafe
utilosafe-1.0.0/README ADDED
@@ -0,0 +1 @@
1
+ # utilosafe
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "utilosafe"
3
+ version = "1.0.0"
4
+ description = ""
5
+ readme = {file = "README", content-type = "text/markdown"}
6
+ requires-python = ">=3.12"
7
+ authors = [
8
+ {name = "Helmut Konrad Schewe", email = "helmutus@outlook.com"}
9
+ ]
10
+
11
+ dependencies = [
12
+ "utilo>=2.107.3,<3.0.0",
13
+ "configos>=1.0.4,<3.0.0",
14
+
15
+ "cryptography>=38.0.3,<39.0.0",
16
+ "bcrypt>=4.0.1,<5.0.0",
17
+ ]
18
+
19
+ # Optional but recommended metadata
20
+ keywords = []
21
+ classifiers = [
22
+ 'Programming Language :: Python :: 3.12',
23
+ 'Programming Language :: Python :: 3.13',
24
+ 'Programming Language :: Python :: 3.14',
25
+ ]
26
+ license = "MIT"
27
+ license-files = ["LICENSE"]
28
+
29
+ [project.scripts]
30
+ safeme_create = "utilosafe.cli:main"
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "utilotest>=0.26.0,<1.0.0",
35
+ "power==2.20.1,<3.0.0",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/anaticulae/utilosafe"
40
+ Repository = "https://github.com/anaticulae/utilosafe"
41
+
42
+ [build-system]
43
+ requires = [
44
+ "setuptools>=82.0.1",
45
+ "wheel>=0.46.3"
46
+ ]
47
+ build-backend = "setuptools.build_meta"
48
+
49
+ [tool.semantic_release]
50
+ version_toml = ["pyproject.toml:project.version"]
51
+ [tool.semantic_release.changelog.default_templates]
52
+ changelog_file = "CHANGELOG"
53
+ [tool.semantic_release.changelog]
54
+ mode = "init"
55
+ output_format = "md"
56
+ [tool.semantic_release.commit_parser_options]
57
+ # allways generate a new version
58
+ patch_tags = ["fix", "perf", "build", "chore", "ci", "docs", "style", "refactor", "test", "deps"]
59
+
60
+ [tool.setuptools.packages.find]
61
+ where = ["."]
62
+ include = [
63
+ "utilosafe",
64
+ ]
65
+ exclude = [
66
+ "tests*",
67
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,46 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import functools
11
+ import os
12
+
13
+ import utilo
14
+ import utilotest
15
+
16
+ import utilosafe
17
+ import utilosafe.cli
18
+
19
+ run = functools.partial(
20
+ utilotest.run_cov,
21
+ process='safeme_create',
22
+ main=utilosafe.cli.main,
23
+ expect=True,
24
+ )
25
+
26
+
27
+ @utilotest.longrun
28
+ def test_generate_cli(testdir, monkeypatch):
29
+ password = b'helm'
30
+ utilo.file_create_binary('password', password)
31
+ with monkeypatch.context() as context:
32
+ context.setattr(
33
+ utilosafe.cli,
34
+ 'insert_password',
35
+ lambda: password,
36
+ )
37
+ # safeme_create < password
38
+ run('', mp=monkeypatch)
39
+ private = str(testdir.tmpdir.join('private.pem'))
40
+ loaded = utilosafe.load_rsa_private( # nosec
41
+ private,
42
+ passphrase=b'helm',
43
+ )
44
+ assert loaded
45
+ assert os.path.exists('private.pem')
46
+ assert os.path.exists('public.pem')
@@ -0,0 +1,66 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ # import power
11
+ import utilotest
12
+
13
+ import utilosafe
14
+ import utilosafe.rsa
15
+
16
+
17
+ @utilotest.longrun
18
+ def test_generate_write_load_rsa(testdir):
19
+ outfile = testdir.tmpdir.join('rsa')
20
+ private = utilosafe.create_private_key()
21
+ expected = utilosafe.create_public_key(private)
22
+ # write file
23
+ utilosafe.write_rsa(outfile, expected)
24
+ # create RSA object
25
+ loaded = utilosafe.rsa.dump_public_key(utilosafe.load_rsa_public(outfile))
26
+ assert loaded == expected
27
+
28
+
29
+ # @pytest.mark.xfail(reason='rework large encryption')
30
+ # @utilotest.nightly
31
+ # def test_write_large_file(testdir):
32
+ # loaded = utilo.file_read_binary(power.BACHELOR128_PDF)
33
+ # private = utilosafe.create_private_key()
34
+ # public = utilosafe.create_public_key(private)
35
+ # # write file
36
+ # encrypted = utilosafe.encrypt(loaded, public)
37
+ # utilo.file_create('blabla', encrypted)
38
+ # decrypted = utilosafe.decrypt(encrypted, private)
39
+ # assert len(decrypted) == len(loaded)
40
+ # assert decrypted == loaded
41
+
42
+
43
+ def test_public_special_bytes():
44
+ """Do not remove starting \x00-zero bits when using rsa decryption."""
45
+ data = (b'\x00\x00\x00\x00\xf4\x0b;\xc77\xdd\xe5\x97\x08'
46
+ b'\xdd2\x12\x9f#\xe0\x98\xd0I\xedv\xd3-\xa6\xa6')
47
+ private = utilosafe.create_private_key()
48
+ public = utilosafe.create_public_key(private)
49
+ encrypted = utilosafe.encrypt(data, public)
50
+ decrypted = utilosafe.decrypt(encrypted, private)
51
+ assert decrypted == data
52
+
53
+
54
+ def test_public_password(testdir, monkeypatch):
55
+ data = b'Helm is Hier'
56
+ private = utilosafe.create_private_key()
57
+ public = utilosafe.create_public_key(private)
58
+ utilosafe.write_rsa('public.pem', public)
59
+ path = str(testdir.tmpdir.join('public.pem'))
60
+ with monkeypatch.context() as context:
61
+ utilosafe.public_password.cache_clear()
62
+ context.setenv(utilosafe.UTILOSAFE_PUBLIC_KEY, path)
63
+ encrypted = utilosafe.encrypt(data)
64
+ assert encrypted != data
65
+ decrypted = utilosafe.decrypt(encrypted, key=private)
66
+ assert decrypted == data
@@ -0,0 +1,29 @@
1
+ #==============================================================================
2
+ # C O P Y R I G H T
3
+ #------------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ #==============================================================================
9
+
10
+ import importlib.metadata
11
+ import os
12
+
13
+ __version__ = importlib.metadata.version('utilosafe')
14
+
15
+ from utilosafe.password import decrypt as decrypt_password
16
+ from utilosafe.password import encrypt as encrypt_password
17
+ from utilosafe.public import decrypt
18
+ from utilosafe.public import encrypt
19
+ from utilosafe.rsa import create_private_key
20
+ from utilosafe.rsa import create_public_key
21
+ from utilosafe.rsa import load_rsa_private
22
+ from utilosafe.rsa import load_rsa_public
23
+ from utilosafe.rsa import write_rsa
24
+ from utilosafe.utils import UTILOSAFE_PUBLIC_KEY
25
+ from utilosafe.utils import UTILOSAFE_USER_PASSWORD
26
+ from utilosafe.utils import public_password
27
+ from utilosafe.utils import user_password
28
+
29
+ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
@@ -0,0 +1,43 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import os
11
+ import sys
12
+
13
+ import utilo
14
+
15
+ import utilosafe
16
+ import utilosafe.rsa
17
+
18
+
19
+ def main():
20
+ cwd = os.getcwd()
21
+ password = insert_password()
22
+
23
+ path_private = os.path.join(cwd, 'private.pem')
24
+ private = utilosafe.rsa.create_private_key(passphrase=password)
25
+ utilo.log(f'write private: {path_private}')
26
+ utilosafe.write_rsa(path_private, private)
27
+
28
+ path_public = os.path.join(cwd, 'public.pem')
29
+ public = utilosafe.rsa.create_public_key(private, passphrase=password)
30
+ utilo.log(f'write public: {path_public}')
31
+ utilosafe.write_rsa(path_public, public)
32
+
33
+ sys.exit(utilo.SUCCESS)
34
+
35
+
36
+ def insert_password():
37
+ password = input('password\n')
38
+ if password:
39
+ password = password.strip()
40
+ password = password.encode('utf8')
41
+ else:
42
+ password = None
43
+ return password
@@ -0,0 +1,64 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import utilo
11
+
12
+ import utilosafe
13
+ import utilosafe.utils
14
+
15
+
16
+ def encrypt(plaintext: bytes) -> bytes:
17
+ r"""Convert plain text to cipher text.
18
+
19
+ >>> len(encrypt(b'hello')) > 25
20
+ True
21
+ >>> decrypt(encrypt(b'hello'))
22
+ b'hello'
23
+ """
24
+ if isinstance(plaintext, str):
25
+ plaintext = plaintext.encode(
26
+ encoding='utf8',
27
+ errors='xmlcharrefreplace',
28
+ )
29
+ plaintext = utilosafe.utils.create_block(plaintext)
30
+ ciphertext = aes().encrypt(plaintext)
31
+ return ciphertext
32
+
33
+
34
+ def decrypt(ciphertext: bytes, string: bool = False) -> bytes:
35
+ """Convert cipher text to plain text."""
36
+ plaintext = aes().decrypt(ciphertext)
37
+ plaintext = utilosafe.utils.remove_block(plaintext)
38
+ if string:
39
+ plaintext: str = plaintext.decode(encoding='utf8')
40
+ return plaintext
41
+
42
+
43
+ def aes():
44
+ pwd = utilosafe.user_password()
45
+ # normalize password
46
+ pwd = utilo.secure_hash(pwd, digits=32)
47
+ pwd: bytes = pwd.encode('utf8')
48
+ # TODO: VERIFY PROS AND CONS OF MODE
49
+ try:
50
+ from Crypto.Cipher import AES # nosec # TODO: VERIFY THIS
51
+ except ImportError:
52
+ utilo.error('could not import Crypto.Cipher')
53
+ return NoPassword()
54
+ result = AES.new(pwd, AES.MODE_ECB)
55
+ return result
56
+
57
+
58
+ class NoPassword:
59
+
60
+ def decrypt(self, ciphertext): # pylint:disable=R0201
61
+ return ciphertext
62
+
63
+ def encrypt(self, plaintext): # pylint:disable=R0201
64
+ return plaintext
@@ -0,0 +1,40 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+ """\
10
+ >>> import utilosafe.rsa
11
+
12
+ >>> PRIVATE = utilosafe.rsa.create_private_key()
13
+ >>> PUBLIC = utilosafe.rsa.create_public_key(PRIVATE)
14
+
15
+ >>> encrypted = encrypt(b'Hello', public=PUBLIC)
16
+ >>> decrypt(encrypted, PRIVATE)
17
+ b'Hello'
18
+ """
19
+
20
+ import utilosafe.rsa
21
+ import utilosafe.utils
22
+
23
+
24
+ def encrypt(plaintext: bytes, public: bytes = None) -> int:
25
+ if public is None:
26
+ public = utilosafe.utils.public_password()
27
+ result = utilosafe.rsa.encrypt(
28
+ plaintext,
29
+ public,
30
+ )
31
+ return result
32
+
33
+
34
+ def decrypt(ciphertext: bytes, key: str, passphrase: bytes = None) -> bytes:
35
+ result = utilosafe.rsa.decrypt(
36
+ ciphertext,
37
+ key,
38
+ passphrase=passphrase,
39
+ )
40
+ return result
@@ -0,0 +1,115 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+ """\
10
+ >>> import utilosafe.rsa
11
+
12
+ >>> PRIVATE = utilosafe.rsa.create_private_key()
13
+ >>> PUBLIC = utilosafe.rsa.create_public_key(PRIVATE)
14
+
15
+ >>> encrypted = encrypt(b'Hello', public=PUBLIC)
16
+ >>> decrypt(encrypted, PRIVATE)
17
+ b'Hello'
18
+ """
19
+
20
+ import utilo
21
+ from cryptography.hazmat.primitives import serialization
22
+ from cryptography.hazmat.primitives.asymmetric import padding
23
+ from cryptography.hazmat.primitives.asymmetric import rsa
24
+
25
+ PUBLIC_EXPONENT = 65537
26
+
27
+ PADDING = padding.PKCS1v15()
28
+
29
+
30
+ def encrypt(plaintext: bytes, public: bytes) -> int:
31
+ if isinstance(public, bytes):
32
+ public = serialization.load_ssh_public_key(public)
33
+ result = public.encrypt(
34
+ plaintext=plaintext,
35
+ padding=PADDING,
36
+ )
37
+ return result
38
+
39
+
40
+ def decrypt(ciphertext: bytes, path: str, passphrase: bytes = None) -> bytes:
41
+ loaded = load_rsa_private(path, passphrase=passphrase)
42
+ result = loaded.decrypt(
43
+ ciphertext=ciphertext,
44
+ padding=PADDING,
45
+ )
46
+ return result
47
+
48
+
49
+ def create_private_key(passphrase: bytes = None, bits: int = 1024) -> bytes:
50
+ r"""\
51
+ >>> create_private_key(b'HelmIsSafe')
52
+ b'-----BEGIN OPENSSH PRIVATE KEY-----...---END OPENSSH PRIVATE KEY-----\n'
53
+ """
54
+ private = rsa.generate_private_key(
55
+ public_exponent=PUBLIC_EXPONENT,
56
+ key_size=bits,
57
+ )
58
+ algo = serialization.NoEncryption()
59
+ if passphrase:
60
+ algo = serialization.BestAvailableEncryption(password=passphrase) # pylint:disable=R0204
61
+ result = private.private_bytes(
62
+ encoding=serialization.Encoding.PEM,
63
+ format=serialization.PrivateFormat.OpenSSH,
64
+ encryption_algorithm=algo,
65
+ )
66
+ return result
67
+
68
+
69
+ def create_public_key(private: bytes, passphrase=None) -> bytes:
70
+ """\
71
+ >>> create_public_key(create_private_key())
72
+ b'ssh-rsa ...=='
73
+ >>> create_public_key(create_private_key(passphrase=b'helm'), b'helm')
74
+ b'ssh-rsa ...=='
75
+ """
76
+ result = serialization.load_ssh_private_key(
77
+ data=private,
78
+ password=passphrase,
79
+ )
80
+ public = result.public_key()
81
+ result = dump_public_key(public)
82
+ return result
83
+
84
+
85
+ def dump_public_key(public) -> bytes:
86
+ result = public.public_bytes(
87
+ encoding=serialization.Encoding.OpenSSH,
88
+ format=serialization.PublicFormat.OpenSSH,
89
+ )
90
+ return result
91
+
92
+
93
+ def write_rsa(path, key: bytes):
94
+ utilo.file_replace_binary(path, key)
95
+
96
+
97
+ def load_rsa_private(
98
+ path,
99
+ passphrase: bytes = None,
100
+ ) -> '_SSH_PRIVATE_KEY_TYPES':
101
+ if isinstance(path, str):
102
+ loaded = utilo.file_read_binary(path)
103
+ else:
104
+ loaded = path
105
+ result = serialization.load_ssh_private_key(
106
+ data=loaded,
107
+ password=passphrase,
108
+ )
109
+ return result
110
+
111
+
112
+ def load_rsa_public(path) -> '_SSH_PUBLIC_KEY_TYPES':
113
+ data = utilo.file_read_binary(path)
114
+ result = serialization.load_ssh_public_key(data=data)
115
+ return result
@@ -0,0 +1,56 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import math
11
+
12
+ import configos
13
+ import utilo
14
+
15
+ import utilosafe
16
+
17
+ BLOCKSIZE = 32
18
+
19
+ UTILOSAFE_PUBLIC_KEY = 'UTILOSAFE_PUBLIC_KEY' # nosec
20
+ UTILOSAFE_USER_PASSWORD = 'UTILOSAFE_USER_PASSWORD' # nosec
21
+
22
+
23
+ def create_block(content: bytes, blocksize: int = BLOCKSIZE) -> bytes:
24
+ """\
25
+ >>> create_block(b'abc' * 15)
26
+ b'abcabcabcabcabcabcabcabcabcabcabcabcabcabcabc '
27
+ """
28
+ if not len(content) % blocksize:
29
+ return content
30
+ missing = math.ceil(len(content) / blocksize) * blocksize - len(content)
31
+ content = content + b' ' * missing
32
+ return content
33
+
34
+
35
+ def remove_block(content: bytes) -> bytes:
36
+ stripped = content.rstrip()
37
+ if content[len(stripped):len(stripped) + 1] == b'\n':
38
+ # valid file ends with newline
39
+ stripped = content[0:len(stripped) + 1]
40
+ return stripped
41
+
42
+
43
+ @utilo.cacheme
44
+ def user_password() -> str:
45
+ result = configos.env(UTILOSAFE_USER_PASSWORD, 'NOPASSWORD')
46
+ return result
47
+
48
+
49
+ @utilo.cacheme
50
+ def public_password() -> 'RSA':
51
+ path = configos.env(UTILOSAFE_PUBLIC_KEY, None)
52
+ if not path:
53
+ utilo.error(f'missing env {UTILOSAFE_PUBLIC_KEY}')
54
+ return None
55
+ result = utilosafe.load_rsa_public(path)
56
+ return result
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: utilosafe
3
+ Version: 1.0.0
4
+ Author-email: Helmut Konrad Schewe <helmutus@outlook.com>
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/anaticulae/utilosafe
7
+ Project-URL: Repository, https://github.com/anaticulae/utilosafe
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: utilo<3.0.0,>=2.107.3
14
+ Requires-Dist: configos<3.0.0,>=1.0.4
15
+ Requires-Dist: cryptography<39.0.0,>=38.0.3
16
+ Requires-Dist: bcrypt<5.0.0,>=4.0.1
17
+ Provides-Extra: dev
18
+ Requires-Dist: utilotest<1.0.0,>=0.26.0; extra == "dev"
19
+ Requires-Dist: power<3.0.0,==2.20.1; extra == "dev"
20
+
21
+ # utilosafe
@@ -0,0 +1,16 @@
1
+ README
2
+ pyproject.toml
3
+ tests/test_cli.py
4
+ tests/test_rsa.py
5
+ utilosafe/__init__.py
6
+ utilosafe/cli.py
7
+ utilosafe/password.py
8
+ utilosafe/public.py
9
+ utilosafe/rsa.py
10
+ utilosafe/utils.py
11
+ utilosafe.egg-info/PKG-INFO
12
+ utilosafe.egg-info/SOURCES.txt
13
+ utilosafe.egg-info/dependency_links.txt
14
+ utilosafe.egg-info/entry_points.txt
15
+ utilosafe.egg-info/requires.txt
16
+ utilosafe.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ safeme_create = utilosafe.cli:main
@@ -0,0 +1,8 @@
1
+ utilo<3.0.0,>=2.107.3
2
+ configos<3.0.0,>=1.0.4
3
+ cryptography<39.0.0,>=38.0.3
4
+ bcrypt<5.0.0,>=4.0.1
5
+
6
+ [dev]
7
+ utilotest<1.0.0,>=0.26.0
8
+ power<3.0.0,==2.20.1
@@ -0,0 +1 @@
1
+ utilosafe