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 +1 -0
- toru_vault/__main__.py +29 -6
- toru_vault/in_env.py +173 -0
- toru_vault/in_memory.py +379 -0
- toru_vault/lazy_dict.py +10 -15
- toru_vault/vault.py +101 -424
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/METADATA +21 -14
- toru_vault-0.2.0.dist-info/RECORD +13 -0
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/WHEEL +1 -1
- toru_vault-0.1.3.dist-info/RECORD +0 -11
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/entry_points.txt +0 -0
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {toru_vault-0.1.3.dist-info → toru_vault-0.2.0.dist-info}/top_level.txt +0 -0
toru_vault/__init__.py
CHANGED
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,
|
11
|
-
_KEYRING_BWS_TOKEN_KEY, _KEYRING_ORG_ID_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
|
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
|
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
|
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}")
|
toru_vault/in_memory.py
ADDED
@@ -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
|
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
|
-
|
8
|
-
|
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
|
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
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
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]
|