vcrypto 4.2.1__tar.gz → 4.3.1__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.
- {vcrypto-4.2.1 → vcrypto-4.3.1}/PKG-INFO +9 -5
- {vcrypto-4.2.1 → vcrypto-4.3.1}/README.md +7 -1
- {vcrypto-4.2.1 → vcrypto-4.3.1}/pyproject.toml +6 -2
- {vcrypto-4.2.1 → vcrypto-4.3.1}/tests/test_encryption.py +23 -1
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto/encryption.py +37 -3
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto/secrets_manager.py +21 -2
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto/vcrypto.py +2 -2
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto.egg-info/PKG-INFO +9 -5
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto.egg-info/requires.txt +0 -2
- {vcrypto-4.2.1 → vcrypto-4.3.1}/setup.cfg +0 -0
- {vcrypto-4.2.1 → vcrypto-4.3.1}/tests/test_secrets_manager.py +0 -0
- {vcrypto-4.2.1 → vcrypto-4.3.1}/tests/test_vcrypto.py +0 -0
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto/__init__.py +0 -0
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto.egg-info/SOURCES.txt +0 -0
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto.egg-info/dependency_links.txt +0 -0
- {vcrypto-4.2.1 → vcrypto-4.3.1}/vcrypto.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: vcrypto
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: Utility to easily store passwords/secrets
|
|
5
5
|
Author-email: Arnau Villoro <arnau@villoro.com>
|
|
6
6
|
Maintainer-email: Arnau Villoro <arnau@villoro.com>
|
|
@@ -21,9 +21,7 @@ Requires-Python: >=3.8
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
Requires-Dist: cryptography>=44.0.1
|
|
23
23
|
Requires-Dist: loguru>=0.7.3
|
|
24
|
-
Requires-Dist: pytest>=8.3.4
|
|
25
24
|
Requires-Dist: pyyaml>=6.0.2
|
|
26
|
-
Requires-Dist: ruff>=0.9.7
|
|
27
25
|
|
|
28
26
|
# **vcrypto - Secure Secrets Management in Python**
|
|
29
27
|
vcrypto is a simple and effective utility for securely storing passwords and secrets in Python. It leverages **Fernet encryption** from the [cryptography](https://cryptography.io/en/latest/) module instead of reinventing the wheel.
|
|
@@ -99,10 +97,16 @@ The `init_vcrypto` function allows customization:
|
|
|
99
97
|
|
|
100
98
|
**Example:**
|
|
101
99
|
```python
|
|
102
|
-
init_vcrypto(secrets_file="config/secrets.yaml", filename_master_password="config/master.
|
|
100
|
+
init_vcrypto(secrets_file="config/secrets.yaml", filename_master_password="config/master.password")
|
|
103
101
|
```
|
|
104
102
|
This setup stores **both** the `master.password` and `secrets.yaml` in the `config/` folder.
|
|
105
103
|
|
|
104
|
+
> [!CAUTION]
|
|
105
|
+
> The master password file must **never** be committed. The bundled `.gitignore`
|
|
106
|
+
> only excludes `*.password`, so if you choose a custom name make sure it is
|
|
107
|
+
> covered by your `.gitignore`. Committing the master password alongside the
|
|
108
|
+
> encrypted `secrets.yaml` defeats the entire scheme.
|
|
109
|
+
|
|
106
110
|
---
|
|
107
111
|
|
|
108
112
|
## **🛠️ Development**
|
|
@@ -72,10 +72,16 @@ The `init_vcrypto` function allows customization:
|
|
|
72
72
|
|
|
73
73
|
**Example:**
|
|
74
74
|
```python
|
|
75
|
-
init_vcrypto(secrets_file="config/secrets.yaml", filename_master_password="config/master.
|
|
75
|
+
init_vcrypto(secrets_file="config/secrets.yaml", filename_master_password="config/master.password")
|
|
76
76
|
```
|
|
77
77
|
This setup stores **both** the `master.password` and `secrets.yaml` in the `config/` folder.
|
|
78
78
|
|
|
79
|
+
> [!CAUTION]
|
|
80
|
+
> The master password file must **never** be committed. The bundled `.gitignore`
|
|
81
|
+
> only excludes `*.password`, so if you choose a custom name make sure it is
|
|
82
|
+
> covered by your `.gitignore`. Committing the master password alongside the
|
|
83
|
+
> encrypted `secrets.yaml` defeats the entire scheme.
|
|
84
|
+
|
|
79
85
|
---
|
|
80
86
|
|
|
81
87
|
## **🛠️ Development**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "vcrypto"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.3.1"
|
|
4
4
|
description = "Utility to easily store passwords/secrets"
|
|
5
5
|
authors = [{ name = "Arnau Villoro", email = "arnau@villoro.com" }]
|
|
6
6
|
maintainers = [{ name = "Arnau Villoro", email = "arnau@villoro.com" }]
|
|
@@ -26,7 +26,11 @@ classifiers = [
|
|
|
26
26
|
dependencies = [
|
|
27
27
|
"cryptography>=44.0.1",
|
|
28
28
|
"loguru>=0.7.3",
|
|
29
|
-
"pytest>=8.3.4",
|
|
30
29
|
"pyyaml>=6.0.2",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.3.4",
|
|
31
35
|
"ruff>=0.9.7",
|
|
32
36
|
]
|
|
@@ -1,12 +1,34 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
1
5
|
from vcrypto.encryption import create_password
|
|
2
6
|
from vcrypto.encryption import decrypt
|
|
3
7
|
from vcrypto.encryption import encrypt
|
|
4
8
|
|
|
9
|
+
TEMP_PASSWORD_FILE = "temp.password"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def cleanup_files():
|
|
14
|
+
"""Ensures test files are removed after each test."""
|
|
15
|
+
yield
|
|
16
|
+
Path(TEMP_PASSWORD_FILE).unlink(missing_ok=True)
|
|
17
|
+
|
|
5
18
|
|
|
6
19
|
def test_store_secret():
|
|
7
20
|
"""Test password creation with storing"""
|
|
8
21
|
|
|
9
|
-
assert create_password(filename=
|
|
22
|
+
assert create_password(filename=TEMP_PASSWORD_FILE) is not None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_create_password_does_not_overwrite():
|
|
26
|
+
"""Refuse to overwrite an existing master password file."""
|
|
27
|
+
|
|
28
|
+
create_password(filename=TEMP_PASSWORD_FILE)
|
|
29
|
+
|
|
30
|
+
with pytest.raises(FileExistsError, match="Refusing to overwrite"):
|
|
31
|
+
create_password(filename=TEMP_PASSWORD_FILE)
|
|
10
32
|
|
|
11
33
|
|
|
12
34
|
def test_encrypt():
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
1
3
|
from typing import Optional
|
|
2
4
|
from typing import Union
|
|
3
5
|
|
|
@@ -5,7 +7,28 @@ from cryptography.fernet import Fernet
|
|
|
5
7
|
from loguru import logger
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
def
|
|
10
|
+
def write_private_file(filename: str, data: Union[str, bytes]) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Writes data to a file readable/writable only by the owner.
|
|
13
|
+
|
|
14
|
+
Uses ``os.open`` with mode ``0o600`` so the restrictive permissions are set
|
|
15
|
+
when the file is created, avoiding a window where it is world-readable. On
|
|
16
|
+
POSIX this yields owner-only access; on Windows the mode is largely ignored
|
|
17
|
+
(access is governed by ACLs) but the call remains harmless.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
filename: Destination path.
|
|
21
|
+
data: Content to write (str or bytes).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
binary = isinstance(data, bytes)
|
|
25
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
|
26
|
+
fd = os.open(filename, flags, 0o600)
|
|
27
|
+
with os.fdopen(fd, "wb" if binary else "w") as file:
|
|
28
|
+
file.write(data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def create_password(filename="master.password", store_secret: bool = True) -> bytes:
|
|
9
32
|
"""
|
|
10
33
|
Creates a new password and stores it in a text file.
|
|
11
34
|
This approach is preferred over allowing users to create their own passwords:
|
|
@@ -17,6 +40,11 @@ def create_password(filename="test.password", store_secret: bool = True) -> byte
|
|
|
17
40
|
|
|
18
41
|
Returns:
|
|
19
42
|
Generated master password as bytes.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
FileExistsError: If the destination file already exists. Overwriting the
|
|
46
|
+
master password would make every secret encrypted with the previous
|
|
47
|
+
key permanently undecryptable, so this must be done deliberately.
|
|
20
48
|
"""
|
|
21
49
|
|
|
22
50
|
# Create a new key
|
|
@@ -27,10 +55,16 @@ def create_password(filename="test.password", store_secret: bool = True) -> byte
|
|
|
27
55
|
if not store_secret:
|
|
28
56
|
return key
|
|
29
57
|
|
|
58
|
+
if Path(filename).exists():
|
|
59
|
+
raise FileExistsError(
|
|
60
|
+
f"Refusing to overwrite the existing master password at {filename!r}. "
|
|
61
|
+
"Delete it manually first if you really intend to generate a new key "
|
|
62
|
+
"(every secret encrypted with the old key will become undecryptable)."
|
|
63
|
+
)
|
|
64
|
+
|
|
30
65
|
logger.info(f"Storing key to {filename=}. Remember to gitignore this file!")
|
|
31
66
|
|
|
32
|
-
|
|
33
|
-
file.write(key.decode())
|
|
67
|
+
write_private_file(filename, key.decode())
|
|
34
68
|
|
|
35
69
|
return key
|
|
36
70
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import tempfile
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import yaml
|
|
@@ -31,7 +32,10 @@ def get_password(filename=FILE_MASTER_DEFAULT, environ_var_name=None):
|
|
|
31
32
|
ValueError: If no password is found.
|
|
32
33
|
"""
|
|
33
34
|
if environ_var_name and (password := os.getenv(environ_var_name)):
|
|
34
|
-
|
|
35
|
+
# Strip to mirror the file branch below; a Fernet key never contains
|
|
36
|
+
# surrounding whitespace, so a stray newline would only cause a
|
|
37
|
+
# confusing decryption failure.
|
|
38
|
+
return password.strip().encode()
|
|
35
39
|
|
|
36
40
|
path = Path(filename)
|
|
37
41
|
if path.exists():
|
|
@@ -65,13 +69,28 @@ def store_dictionary(data: dict, filename: str):
|
|
|
65
69
|
"""
|
|
66
70
|
Stores a dictionary in a JSON or YAML file.
|
|
67
71
|
|
|
72
|
+
The file is written atomically (to a temporary file in the same directory,
|
|
73
|
+
then renamed) so an interrupted write cannot truncate or corrupt the
|
|
74
|
+
existing secrets file.
|
|
75
|
+
|
|
68
76
|
Args:
|
|
69
77
|
data (dict): Dictionary to store.
|
|
70
78
|
filename (str): Destination file.
|
|
71
79
|
"""
|
|
72
80
|
path = Path(filename)
|
|
73
81
|
content = json.dumps(data, indent=2) if _is_json(path) else yaml.dump(data)
|
|
74
|
-
|
|
82
|
+
|
|
83
|
+
# Write to a temp file in the same directory, then atomically replace the
|
|
84
|
+
# target. os.replace is atomic on the same filesystem on POSIX and Windows.
|
|
85
|
+
fd, tmp_name = tempfile.mkstemp(dir=path.parent or ".", suffix=".tmp")
|
|
86
|
+
try:
|
|
87
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
88
|
+
stream.write(content)
|
|
89
|
+
os.replace(tmp_name, path)
|
|
90
|
+
except BaseException:
|
|
91
|
+
if os.path.exists(tmp_name):
|
|
92
|
+
os.unlink(tmp_name)
|
|
93
|
+
raise
|
|
75
94
|
|
|
76
95
|
|
|
77
96
|
def read_dictionary(filename: str, fail_if_missing=True) -> dict:
|
|
@@ -95,5 +95,5 @@ def export_secret(uri, secret_name, binary=False):
|
|
|
95
95
|
secret = get_secret(secret_name, encoding="utf8" if not binary else None)
|
|
96
96
|
|
|
97
97
|
logger.info(f"Writing {secret_name=} to {uri=}")
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
# The exported file holds the decrypted secret, so restrict it to the owner.
|
|
99
|
+
encryption.write_private_file(uri, secret)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: vcrypto
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: Utility to easily store passwords/secrets
|
|
5
5
|
Author-email: Arnau Villoro <arnau@villoro.com>
|
|
6
6
|
Maintainer-email: Arnau Villoro <arnau@villoro.com>
|
|
@@ -21,9 +21,7 @@ Requires-Python: >=3.8
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
Requires-Dist: cryptography>=44.0.1
|
|
23
23
|
Requires-Dist: loguru>=0.7.3
|
|
24
|
-
Requires-Dist: pytest>=8.3.4
|
|
25
24
|
Requires-Dist: pyyaml>=6.0.2
|
|
26
|
-
Requires-Dist: ruff>=0.9.7
|
|
27
25
|
|
|
28
26
|
# **vcrypto - Secure Secrets Management in Python**
|
|
29
27
|
vcrypto is a simple and effective utility for securely storing passwords and secrets in Python. It leverages **Fernet encryption** from the [cryptography](https://cryptography.io/en/latest/) module instead of reinventing the wheel.
|
|
@@ -99,10 +97,16 @@ The `init_vcrypto` function allows customization:
|
|
|
99
97
|
|
|
100
98
|
**Example:**
|
|
101
99
|
```python
|
|
102
|
-
init_vcrypto(secrets_file="config/secrets.yaml", filename_master_password="config/master.
|
|
100
|
+
init_vcrypto(secrets_file="config/secrets.yaml", filename_master_password="config/master.password")
|
|
103
101
|
```
|
|
104
102
|
This setup stores **both** the `master.password` and `secrets.yaml` in the `config/` folder.
|
|
105
103
|
|
|
104
|
+
> [!CAUTION]
|
|
105
|
+
> The master password file must **never** be committed. The bundled `.gitignore`
|
|
106
|
+
> only excludes `*.password`, so if you choose a custom name make sure it is
|
|
107
|
+
> covered by your `.gitignore`. Committing the master password alongside the
|
|
108
|
+
> encrypted `secrets.yaml` defeats the entire scheme.
|
|
109
|
+
|
|
106
110
|
---
|
|
107
111
|
|
|
108
112
|
## **🛠️ Development**
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|