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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: vcrypto
3
- Version: 4.2.1
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.secret")
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.secret")
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.2.1"
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="temp.password") is not None
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 create_password(filename="test.password", store_secret: bool = True) -> bytes:
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
- with open(filename, "w") as file:
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
- return password.encode()
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
- path.write_text(content, encoding="utf-8")
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
- with open(uri, "wb" if binary else "w") as stream:
99
- stream.write(secret)
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.2
1
+ Metadata-Version: 2.4
2
2
  Name: vcrypto
3
- Version: 4.2.1
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.secret")
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**
@@ -1,5 +1,3 @@
1
1
  cryptography>=44.0.1
2
2
  loguru>=0.7.3
3
- pytest>=8.3.4
4
3
  pyyaml>=6.0.2
5
- ruff>=0.9.7
File without changes
File without changes
File without changes