toru-vault 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
toru_vault/__init__.py CHANGED
@@ -2,4 +2,5 @@
2
2
  # Import functions from the core module
3
3
  from .vault import env_load, env_load_all, get, get_all
4
4
 
5
+ __version__ = "0.2.0"
5
6
  __all__ = ["env_load", "env_load_all", "get", "get_all"]
toru_vault/__main__.py CHANGED
@@ -7,9 +7,32 @@ import os
7
7
  import sys
8
8
  import getpass
9
9
 
10
- from .vault import (_initialize_client, set_to_keyring, _get_from_keyring_or_env,
11
- _KEYRING_BWS_TOKEN_KEY, _KEYRING_ORG_ID_KEY, _KEYRING_STATE_FILE_KEY,
12
- _KEYRING_AVAILABLE)
10
+ from .vault import (_initialize_client, _get_from_keyring_or_env,
11
+ _KEYRING_SERVICE_NAME, _KEYRING_BWS_TOKEN_KEY, _KEYRING_ORG_ID_KEY,
12
+ _KEYRING_STATE_FILE_KEY, _KEYRING_AVAILABLE)
13
+
14
+
15
+ def _set_to_keyring(key, value):
16
+ """
17
+ Set a value to keyring
18
+
19
+ Args:
20
+ key (str): Key in keyring
21
+ value (str): Value to store
22
+
23
+ Returns:
24
+ bool: True if successful, False otherwise
25
+ """
26
+ if not _KEYRING_AVAILABLE:
27
+ return False
28
+
29
+ try:
30
+ import keyring
31
+ keyring.set_password(_KEYRING_SERVICE_NAME, key, value)
32
+ return True
33
+ except Exception as e:
34
+ print(f"Failed to set {key} to keyring: {e}")
35
+ return False
13
36
 
14
37
 
15
38
  def list_projects(organization_id=None):
@@ -103,21 +126,21 @@ def init_vault():
103
126
  # Store in keyring
104
127
  if _KEYRING_AVAILABLE:
105
128
  if existing_token != token or not existing_token:
106
- if set_to_keyring(_KEYRING_BWS_TOKEN_KEY, token):
129
+ if _set_to_keyring(_KEYRING_BWS_TOKEN_KEY, token):
107
130
  print("BWS_TOKEN stored in keyring")
108
131
  else:
109
132
  print("Failed to store BWS_TOKEN in keyring")
110
133
  return False
111
134
 
112
135
  if existing_org_id != org_id or not existing_org_id:
113
- if set_to_keyring(_KEYRING_ORG_ID_KEY, org_id):
136
+ if _set_to_keyring(_KEYRING_ORG_ID_KEY, org_id):
114
137
  print("ORGANIZATION_ID stored in keyring")
115
138
  else:
116
139
  print("Failed to store ORGANIZATION_ID in keyring")
117
140
  return False
118
141
 
119
142
  if existing_state_file != state_file or not existing_state_file:
120
- if set_to_keyring(_KEYRING_STATE_FILE_KEY, state_file):
143
+ if _set_to_keyring(_KEYRING_STATE_FILE_KEY, state_file):
121
144
  print("STATE_FILE stored in keyring")
122
145
  else:
123
146
  print("Failed to store STATE_FILE in keyring")
toru_vault/in_env.py ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import logging
4
+ from typing import Dict
5
+
6
+ # Setup minimal logging
7
+ logger = logging.getLogger(__name__)
8
+
9
+ def set_env_vars(secrets: Dict[str, str], override: bool = False) -> None:
10
+ """
11
+ Set environment variables based on secrets dictionary
12
+
13
+ Args:
14
+ secrets: Dictionary of secrets
15
+ override: Whether to override existing environment variables
16
+ """
17
+ # Set environment variables
18
+ for key, value in secrets.items():
19
+ if override or key not in os.environ:
20
+ os.environ[key] = value
21
+
22
+ def process_env_project(project_id: str, project_name: str, override: bool, load_project_secrets) -> None:
23
+ """
24
+ Process a project and load its secrets into environment variables
25
+
26
+ Args:
27
+ project_id: Project ID
28
+ project_name: Project name (for logging)
29
+ override: Whether to override existing environment variables
30
+ load_project_secrets: Function that loads secrets for a specific project
31
+ """
32
+ try:
33
+ # Get the secrets for this project and set them as environment variables
34
+ secrets = load_project_secrets(project_id)
35
+ set_env_vars(secrets, override)
36
+ logger.info(f"Loaded secrets from project: {project_name}")
37
+ except Exception as e:
38
+ logger.warning(f"Failed to load secrets from project {project_id}: {e}")
39
+
40
+ def load_secrets_env(client, organization_id, project_id=None):
41
+ """
42
+ Load secrets from Bitwarden specifically for environment variable usage
43
+
44
+ Args:
45
+ client: Initialized Bitwarden client
46
+ organization_id: Organization ID
47
+ project_id: Optional project ID to filter secrets
48
+
49
+ Returns:
50
+ dict: Dictionary of secrets with their names as keys
51
+ """
52
+ try:
53
+ client.secrets().sync(organization_id, None)
54
+
55
+ secrets = {}
56
+
57
+ # Retrieve all secrets details (no values)
58
+ all_secrets = client.secrets().list(organization_id)
59
+
60
+ if not hasattr(all_secrets, 'data') or not hasattr(all_secrets.data, 'data'):
61
+ return {}
62
+
63
+ secret_ids = []
64
+ for secret in all_secrets.data.data:
65
+ secret_ids.append(secret.id)
66
+
67
+ if secret_ids:
68
+ # Fetch value for each secret
69
+ secrets_detailed = client.secrets().get_by_ids(secret_ids)
70
+
71
+ if not hasattr(secrets_detailed, 'data') or not hasattr(secrets_detailed.data, 'data'):
72
+ return {}
73
+
74
+ # Process each secret
75
+ for secret in secrets_detailed.data.data:
76
+ # Extract the project ID
77
+ secret_project_id = getattr(secret, 'project_id', None)
78
+
79
+ # Check if this secret belongs to the specified project
80
+ if project_id and secret_project_id is not None and project_id != str(secret_project_id):
81
+ continue
82
+
83
+ # Add the secret to our dictionary
84
+ secrets[secret.key] = secret.value
85
+
86
+ return secrets
87
+ except Exception as e:
88
+ logger.error(f"Error loading secrets for environment: {e}")
89
+ return {}
90
+
91
+ def load_secrets_env_all(client, organization_id):
92
+ """
93
+ Load all secrets from all projects in the organization
94
+
95
+ Args:
96
+ client: Bitwarden client instance
97
+ organization_id: Organization ID
98
+
99
+ Returns:
100
+ dict: Dictionary of all secrets from all projects
101
+ """
102
+ try:
103
+ # Sync secrets with server
104
+ client.secrets().sync(organization_id, None)
105
+
106
+ # Get all secrets directly
107
+ all_secrets_list = client.secrets().list(organization_id)
108
+ if not hasattr(all_secrets_list, 'data') or not hasattr(all_secrets_list.data, 'data'):
109
+ return {}
110
+
111
+ # Extract secret IDs
112
+ secret_ids = [secret.id for secret in all_secrets_list.data.data]
113
+
114
+ if not secret_ids:
115
+ return {}
116
+
117
+ # Fetch values for all secrets
118
+ secrets_detailed = client.secrets().get_by_ids(secret_ids)
119
+ if not hasattr(secrets_detailed, 'data') or not hasattr(secrets_detailed.data, 'data'):
120
+ return {}
121
+
122
+ # Process all secrets
123
+ all_secrets = {}
124
+ for secret in secrets_detailed.data.data:
125
+ all_secrets[secret.key] = secret.value
126
+
127
+ return all_secrets
128
+ except Exception as e:
129
+ logger.error(f"Error loading secrets from all projects: {e}")
130
+ return {}
131
+
132
+
133
+ def process_all_projects(organization_id: str, override: bool,
134
+ initialize_client, load_project_secrets) -> None:
135
+ """
136
+ Process all projects and load their secrets into environment variables
137
+
138
+ Args:
139
+ organization_id: Organization ID
140
+ override: Whether to override existing environment variables
141
+ initialize_client: Function that initializes the Bitwarden client
142
+ load_project_secrets: Function that loads secrets for a specific project
143
+ """
144
+ # Initialize Bitwarden client
145
+ try:
146
+ client = initialize_client()
147
+ except Exception as e:
148
+ logger.error(f"Failed to initialize Bitwarden client: {e}")
149
+ return
150
+
151
+ try:
152
+ # Sync to ensure we have the latest data
153
+ client.secrets().sync(organization_id, None)
154
+
155
+ # Get all projects
156
+ projects_response = client.projects().list(organization_id)
157
+
158
+ # Validate response format
159
+ if not hasattr(projects_response, 'data') or not hasattr(projects_response.data, 'data'):
160
+ logger.warning(f"No projects found in organization {organization_id}")
161
+ return
162
+
163
+ # Process each project
164
+ for project in projects_response.data.data:
165
+ if hasattr(project, 'id'):
166
+ project_id = str(project.id)
167
+ project_name = getattr(project, 'name', project_id)
168
+
169
+ # Load environment variables for this project
170
+ process_env_project(project_id, project_name, override, load_project_secrets)
171
+
172
+ except Exception as e:
173
+ logger.error(f"Failed to load all secrets into environment variables: {e}")
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import logging
4
+ import tempfile
5
+ import stat
6
+ import secrets as pysecrets
7
+ from typing import Dict, Optional, Tuple, Set
8
+ from cryptography.fernet import Fernet
9
+ from cryptography.hazmat.primitives import hashes
10
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
11
+ import base64
12
+ from .lazy_dict import LazySecretsDict
13
+
14
+ # Try importing keyring - it might not be available in container environments
15
+ try:
16
+ import keyring
17
+ _KEYRING_AVAILABLE = True
18
+ except ImportError:
19
+ _KEYRING_AVAILABLE = False
20
+
21
+ # Setup minimal logging
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Constants for keyring storage
25
+ _KEYRING_SERVICE_NAME = "bitwarden_vault"
26
+ _KEYRING_BWS_TOKEN_KEY = "bws_token"
27
+ _KEYRING_ORG_ID_KEY = "organization_id"
28
+
29
+ # No caching - encryption and decryption only happens JIT
30
+
31
+ def _generate_encryption_key(salt: bytes = None) -> Tuple[bytes, bytes]:
32
+ """
33
+ Generate an encryption key for securing the cache
34
+
35
+ Args:
36
+ salt (bytes, optional): Salt for key derivation
37
+
38
+ Returns:
39
+ Tuple[bytes, bytes]: Key and salt
40
+ """
41
+ if salt is None:
42
+ salt = os.urandom(16)
43
+
44
+ # Generate a key from the machine-specific information and random salt
45
+ machine_id = _get_machine_id()
46
+ password = machine_id.encode()
47
+
48
+ kdf = PBKDF2HMAC(
49
+ algorithm=hashes.SHA256(),
50
+ length=32,
51
+ salt=salt,
52
+ iterations=100000,
53
+ )
54
+ key = base64.urlsafe_b64encode(kdf.derive(password))
55
+ return key, salt
56
+
57
+ def _get_machine_id() -> str:
58
+ """Get a unique identifier for the current machine"""
59
+ machine_id = ""
60
+ # MacOS and Linux
61
+ if os.path.exists('/etc/machine-id'):
62
+ with open('/etc/machine-id', 'r') as f:
63
+ machine_id = f.read().strip()
64
+ elif os.path.exists('/var/lib/dbus/machine-id'):
65
+ with open('/var/lib/dbus/machine-id', 'r') as f:
66
+ machine_id = f.read().strip()
67
+ elif os.name == 'nt': # Windows
68
+ import subprocess
69
+ try:
70
+ result = subprocess.run(['wmic', 'csproduct', 'get', 'UUID'], capture_output=True, text=True)
71
+ if result.returncode == 0:
72
+ machine_id = result.stdout.strip().split('\n')[-1].strip()
73
+ except (FileNotFoundError, subprocess.SubprocessError):
74
+ pass
75
+
76
+ if not machine_id:
77
+ # Some systems truncate nodename to 8 characters or to the leading component;
78
+ # a better way to get the hostname is socket.gethostname()
79
+ import socket
80
+ hostname = socket.gethostname()
81
+
82
+ # Create a persistent random ID
83
+ id_file = os.path.join(tempfile.gettempdir(), '.vault_machine_id')
84
+ if os.path.exists(id_file):
85
+ try:
86
+ with open(id_file, 'r') as f:
87
+ random_id = f.read().strip()
88
+ except Exception:
89
+ random_id = pysecrets.token_hex(16)
90
+ else:
91
+ random_id = pysecrets.token_hex(16)
92
+ try:
93
+ # Try to save it with restricted permissions
94
+ with open(id_file, 'w') as f:
95
+ f.write(random_id)
96
+ os.chmod(id_file, stat.S_IRUSR | stat.S_IWUSR) # 0600 permissions
97
+ except Exception:
98
+ pass
99
+
100
+ machine_id = f"{hostname}-{random_id}"
101
+
102
+ return machine_id
103
+
104
+ def _encrypt_secret(secret_value: str) -> Optional[str]:
105
+ """
106
+ Encrypt a single secret value
107
+
108
+ Args:
109
+ secret_value (str): Secret value to encrypt
110
+
111
+ Returns:
112
+ Optional[str]: Encrypted data or None if encryption fails
113
+ """
114
+ try:
115
+ key, salt = _generate_encryption_key()
116
+ if not key:
117
+ return None
118
+
119
+ # Encrypt the secret value
120
+ f = Fernet(key)
121
+ encrypted_data = f.encrypt(secret_value.encode())
122
+
123
+ # Store along with the salt
124
+ return base64.urlsafe_b64encode(salt).decode() + ":" + encrypted_data.decode()
125
+ except Exception as e:
126
+ logger.warning(f"Failed to encrypt secret: {e}")
127
+ return None
128
+
129
+ def _encrypt_secrets(secrets_dict: Dict[str, str]) -> Optional[Dict[str, str]]:
130
+ """
131
+ Encrypt secrets dictionary with per-secret encryption
132
+
133
+ Args:
134
+ secrets_dict (Dict[str, str]): Dictionary of secrets
135
+
136
+ Returns:
137
+ Optional[Dict[str, str]]: Dictionary of encrypted secrets or None if encryption fails
138
+ """
139
+ try:
140
+ encrypted_secrets = {}
141
+ for key, value in secrets_dict.items():
142
+ encrypted_value = _encrypt_secret(value)
143
+ if encrypted_value:
144
+ encrypted_secrets[key] = encrypted_value
145
+ else:
146
+ logger.warning(f"Failed to encrypt secret '{key}'")
147
+
148
+ return encrypted_secrets if encrypted_secrets else None
149
+ except Exception as e:
150
+ logger.warning(f"Failed to encrypt secrets: {e}")
151
+ return None
152
+
153
+ def _decrypt_secret(encrypted_value: str) -> Optional[str]:
154
+ """
155
+ Decrypt a single secret value
156
+
157
+ Args:
158
+ encrypted_value (str): Encrypted secret value
159
+
160
+ Returns:
161
+ Optional[str]: Decrypted secret value or None if decryption fails
162
+ """
163
+ try:
164
+ # Split salt and encrypted data
165
+ salt_b64, encrypted = encrypted_value.split(":", 1)
166
+ salt = base64.urlsafe_b64decode(salt_b64)
167
+
168
+ # Regenerate the key with the same salt
169
+ key, _ = _generate_encryption_key(salt)
170
+ if not key:
171
+ return None
172
+
173
+ # Decrypt the data
174
+ f = Fernet(key)
175
+ decrypted_data = f.decrypt(encrypted.encode())
176
+
177
+ return decrypted_data.decode()
178
+ except Exception as e:
179
+ logger.warning(f"Failed to decrypt secret: {e}")
180
+ return None
181
+
182
+ def _decrypt_secrets(encrypted_dict: Dict[str, str]) -> Optional[Dict[str, str]]:
183
+ """
184
+ Decrypt a dictionary of encrypted secrets
185
+
186
+ Args:
187
+ encrypted_dict (Dict[str, str]): Dictionary of encrypted secrets
188
+
189
+ Returns:
190
+ Optional[Dict[str, str]]: Decrypted secrets dictionary or None if decryption fails
191
+ """
192
+ try:
193
+ decrypted_secrets = {}
194
+ for key, encrypted_value in encrypted_dict.items():
195
+ decrypted_value = _decrypt_secret(encrypted_value)
196
+ if decrypted_value is not None:
197
+ decrypted_secrets[key] = decrypted_value
198
+
199
+ return decrypted_secrets if decrypted_secrets else None
200
+ except Exception as e:
201
+ logger.warning(f"Failed to decrypt secrets dictionary: {e}")
202
+ return None
203
+
204
+ def create_secrets_dict(secrets_keys: Set[str], organization_id: str, project_id: str,
205
+ all_secrets: Dict[str, str], use_keyring: bool) -> LazySecretsDict:
206
+ """
207
+ Create a LazySecretsDict with appropriate getter, setter, and deleter functions
208
+
209
+ Args:
210
+ secrets_keys: Set of keys available in the dictionary
211
+ organization_id: Organization ID
212
+ project_id: Project ID
213
+ all_secrets: Dictionary of all preloaded secrets
214
+ refresh: Whether to force refresh
215
+ use_keyring: Whether to use keyring
216
+
217
+ Returns:
218
+ LazySecretsDict: Dictionary of secrets with lazy loading
219
+ """
220
+ # Build the service name for keyring storage
221
+ service_name = f"vault_{organization_id or 'default'}"
222
+
223
+ # When keyring is unavailable or not requested (likely in container)
224
+ keyring_usable = _KEYRING_AVAILABLE and use_keyring
225
+
226
+ if keyring_usable:
227
+ # Store encrypted secrets in keyring if available and requested
228
+ for key, value in all_secrets.items():
229
+ encrypted_value = _encrypt_secret(value)
230
+ if encrypted_value:
231
+ keyring.set_password(service_name, key, encrypted_value)
232
+
233
+ # Create getter function for keyring mode that decrypts JIT
234
+ def _keyring_getter(key):
235
+ encrypted_value = keyring.get_password(service_name, key)
236
+ if encrypted_value:
237
+ return _decrypt_secret(encrypted_value)
238
+ return None
239
+
240
+ # Create setter function for keyring mode
241
+ def _keyring_setter(key, value):
242
+ encrypted_value = _encrypt_secret(value)
243
+ if encrypted_value:
244
+ keyring.set_password(service_name, key, encrypted_value)
245
+
246
+ # Create deleter function for keyring mode
247
+ def _keyring_deleter(key):
248
+ keyring.delete_password(service_name, key)
249
+
250
+ # Create the lazy dictionary with keyring functions
251
+ return LazySecretsDict(secrets_keys, _keyring_getter, _keyring_setter, _keyring_deleter)
252
+ else:
253
+ # Container or non-keyring mode implementation
254
+ # No caching - always work with encrypted provided secrets
255
+
256
+ # Function to load secrets from Bitwarden - defined as forward reference
257
+ # This will be passed in by the vault.py module when calling this function
258
+ _load_secrets = None
259
+
260
+ # Create getter function for container mode with JIT decryption
261
+ def _container_getter(key):
262
+ nonlocal _load_secrets
263
+
264
+ # If value exists in memory (either plaintext or encrypted)
265
+ if all_secrets and key in all_secrets:
266
+ value = all_secrets[key]
267
+
268
+ # Check if the value is encrypted (has the salt:encrypted format)
269
+ if isinstance(value, str) and ":" in value:
270
+ # Decrypt the value but don't store decrypted version
271
+ decrypted = _decrypt_secret(value)
272
+ if decrypted is not None:
273
+ return decrypted
274
+ # If decryption fails, return the original value (might be plaintext)
275
+ return value
276
+ # Return plaintext value if not encrypted
277
+ return value
278
+
279
+ # If all else fails, load from API
280
+ if _load_secrets:
281
+ fresh_secrets = _load_secrets(project_id)
282
+ if key in fresh_secrets:
283
+ # Don't save in container_secrets to avoid storing plaintext
284
+ # Just return the value
285
+ return fresh_secrets[key]
286
+
287
+ return None
288
+
289
+ # Create the lazy dictionary with container getter
290
+ return LazySecretsDict(secrets_keys, _container_getter)
291
+
292
+ # No caching functionality
293
+
294
+ def load_secrets_memory(client, organization_id, project_id=None):
295
+ """
296
+ Load secrets from Bitwarden specifically for in-memory usage
297
+
298
+ Args:
299
+ client: Initialized Bitwarden client
300
+ organization_id: Organization ID
301
+ project_id: Optional project ID to filter secrets
302
+
303
+ Returns:
304
+ dict: Dictionary of secrets with their names as keys (encrypted in memory)
305
+ """
306
+ try:
307
+ client.secrets().sync(organization_id, None)
308
+
309
+ secrets = {}
310
+ encrypted_secrets = {}
311
+
312
+ # Retrieve all secrets details (no values)
313
+ all_secrets = client.secrets().list(organization_id)
314
+
315
+ if not hasattr(all_secrets, 'data') or not hasattr(all_secrets.data, 'data'):
316
+ return {}
317
+
318
+ secret_ids = []
319
+ for secret in all_secrets.data.data:
320
+ secret_ids.append(secret.id)
321
+
322
+ if secret_ids:
323
+ # Fetch value for each secret
324
+ secrets_detailed = client.secrets().get_by_ids(secret_ids)
325
+
326
+ if not hasattr(secrets_detailed, 'data') or not hasattr(secrets_detailed.data, 'data'):
327
+ return {}
328
+
329
+ # Process each secret
330
+ for secret in secrets_detailed.data.data:
331
+ # Extract the project ID
332
+ secret_project_id = getattr(secret, 'project_id', None)
333
+
334
+ # Check if this secret belongs to the specified project
335
+ if project_id and secret_project_id is not None and project_id != str(secret_project_id):
336
+ continue
337
+
338
+ # Add the secret to our dictionary (plaintext)
339
+ secrets[secret.key] = secret.value
340
+
341
+ # Also encrypt the value for in-memory storage
342
+ encrypted_value = _encrypt_secret(secret.value)
343
+ if encrypted_value:
344
+ encrypted_secrets[secret.key] = encrypted_value
345
+ else:
346
+ # Fallback if encryption fails
347
+ encrypted_secrets[secret.key] = secret.value
348
+
349
+ # Return the encrypted secrets for secure in-memory storage
350
+ # Values will be decrypted JIT when accessed
351
+ return encrypted_secrets
352
+ except Exception as e:
353
+ logger.error(f"Error loading secrets for memory storage: {e}")
354
+ return {}
355
+
356
+ def decrypt_cached_secrets(organization_id: str, project_id: str):
357
+ """
358
+ Function retained for API compatibility but always returns None as caching is disabled
359
+
360
+ Args:
361
+ organization_id: Organization ID
362
+ project_id: Project ID
363
+
364
+ Returns:
365
+ None: Always returns None as caching is disabled
366
+ """
367
+ return None
368
+
369
+ def update_secrets_cache(organization_id: str, project_id: str, secrets: Dict[str, str]) -> None:
370
+ """
371
+ Function retained for API compatibility but does nothing as caching is disabled
372
+
373
+ Args:
374
+ organization_id: Organization ID
375
+ project_id: Project ID
376
+ secrets: Dictionary of secrets
377
+ """
378
+ # No-op since caching is disabled
379
+ return
toru_vault/lazy_dict.py CHANGED
@@ -1,11 +1,12 @@
1
1
  from collections.abc import MutableMapping
2
- from typing import Dict, Optional, Callable, Set, Iterator, Tuple
2
+ from typing import Optional, Callable, Set, Iterator, Tuple
3
3
 
4
4
  class LazySecretsDict(MutableMapping):
5
5
  """
6
6
  A dictionary-like class that only loads/decrypts secrets when they are accessed.
7
- When container=True, it uses the existing encryption methods.
8
- When container=False, it uses OS keyring for secure storage.
7
+ The specific mechanisms for fetching and storing secrets (e.g., using OS keyring
8
+ or other encryption methods) are determined by the functions provided during
9
+ its instantiation.
9
10
  """
10
11
 
11
12
  def __init__(self,
@@ -26,21 +27,18 @@ class LazySecretsDict(MutableMapping):
26
27
  self._getter = getter_func
27
28
  self._setter = setter_func
28
29
  self._deleter = deleter_func
29
- self._cache: Dict[str, str] = {}
30
30
 
31
31
  def __getitem__(self, key: str) -> str:
32
- """Get an item from the dictionary, fetching/decrypting it on first access."""
32
+ """Get an item from the dictionary, fetching/decrypting it on each access."""
33
33
  if key not in self._keys:
34
34
  raise KeyError(key)
35
35
 
36
- # If not in cache, fetch it
37
- if key not in self._cache:
38
- value = self._getter(key)
39
- if value is None:
40
- raise KeyError(f"Failed to retrieve value for key: {key}")
41
- self._cache[key] = value
36
+ # Always fetch and decrypt fresh to avoid keeping decrypted values in memory
37
+ value = self._getter(key)
38
+ if value is None:
39
+ raise KeyError(f"Failed to retrieve value for key: {key}")
42
40
 
43
- return self._cache[key]
41
+ return value
44
42
 
45
43
  def __setitem__(self, key: str, value: str) -> None:
46
44
  """Set an item in the dictionary."""
@@ -73,15 +71,12 @@ class LazySecretsDict(MutableMapping):
73
71
  return len(self._keys)
74
72
 
75
73
  def items(self) -> Iterator[Tuple[str, str]]:
76
- """Return an iterator over (key, value) pairs."""
77
74
  for key in self._keys:
78
75
  yield (key, self[key])
79
76
 
80
77
  def keys(self) -> Set[str]:
81
- """Return the set of keys."""
82
78
  return self._keys.copy()
83
79
 
84
80
  def values(self) -> Iterator[str]:
85
- """Return an iterator over values."""
86
81
  for key in self._keys:
87
82
  yield self[key]