toru-vault 0.1.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 +5 -0
- toru_vault/__main__.py +182 -0
- toru_vault/lazy_dict.py +87 -0
- toru_vault/py.typed +1 -0
- toru_vault/vault.py +532 -0
- toru_vault-0.1.0.dist-info/METADATA +250 -0
- toru_vault-0.1.0.dist-info/RECORD +11 -0
- toru_vault-0.1.0.dist-info/WHEEL +5 -0
- toru_vault-0.1.0.dist-info/entry_points.txt +2 -0
- toru_vault-0.1.0.dist-info/licenses/LICENSE +21 -0
- toru_vault-0.1.0.dist-info/top_level.txt +1 -0
toru_vault/__init__.py
ADDED
toru_vault/__main__.py
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Command-line interface for the vault package.
|
4
|
+
"""
|
5
|
+
import argparse
|
6
|
+
import os
|
7
|
+
import sys
|
8
|
+
import getpass
|
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)
|
13
|
+
|
14
|
+
|
15
|
+
def list_projects(organization_id=None):
|
16
|
+
"""
|
17
|
+
List all projects and their IDs for the given organization.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
organization_id (str, optional): Organization ID
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
list: List of projects
|
24
|
+
"""
|
25
|
+
# Check for organization ID
|
26
|
+
if not organization_id:
|
27
|
+
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
28
|
+
if not organization_id:
|
29
|
+
print("Error: ORGANIZATION_ID not found in keyring or environment variable")
|
30
|
+
sys.exit(1)
|
31
|
+
|
32
|
+
try:
|
33
|
+
# Initialize client
|
34
|
+
client = _initialize_client()
|
35
|
+
|
36
|
+
# Get all projects
|
37
|
+
projects = client.projects().list(organization_id)
|
38
|
+
|
39
|
+
if not hasattr(projects, 'data') or not hasattr(projects.data, 'data'):
|
40
|
+
print("No projects found or invalid response format")
|
41
|
+
return []
|
42
|
+
|
43
|
+
return projects.data.data
|
44
|
+
except Exception as e:
|
45
|
+
print(f"Error listing projects: {e}")
|
46
|
+
sys.exit(1)
|
47
|
+
|
48
|
+
|
49
|
+
def init_vault():
|
50
|
+
"""
|
51
|
+
Initialize vault by storing BWS_TOKEN, ORGANIZATION_ID, and STATE_FILE in keyring.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
bool: True if initialization was successful
|
55
|
+
"""
|
56
|
+
# Check if keyring is available
|
57
|
+
if not _KEYRING_AVAILABLE:
|
58
|
+
print("Error: keyring package is not available. Cannot securely store credentials.")
|
59
|
+
return False
|
60
|
+
|
61
|
+
# Get existing values
|
62
|
+
existing_token = _get_from_keyring_or_env(_KEYRING_BWS_TOKEN_KEY, "BWS_TOKEN")
|
63
|
+
existing_org_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
64
|
+
existing_state_file = _get_from_keyring_or_env(_KEYRING_STATE_FILE_KEY, "STATE_FILE")
|
65
|
+
|
66
|
+
# Suggest current directory for STATE_FILE if not set
|
67
|
+
current_dir = os.getcwd()
|
68
|
+
suggested_state_file = os.path.join(current_dir, "state")
|
69
|
+
|
70
|
+
# Ask for BWS_TOKEN or use existing
|
71
|
+
if existing_token:
|
72
|
+
print(f"Found existing BWS_TOKEN {'in keyring' if _KEYRING_AVAILABLE else 'in environment'}")
|
73
|
+
new_token = getpass.getpass("Enter new BWS_TOKEN (leave empty to keep existing): ")
|
74
|
+
token = new_token if new_token else existing_token
|
75
|
+
else:
|
76
|
+
token = getpass.getpass("Enter BWS_TOKEN: ")
|
77
|
+
if not token:
|
78
|
+
print("Error: BWS_TOKEN is required")
|
79
|
+
return False
|
80
|
+
|
81
|
+
# Ask for ORGANIZATION_ID or use existing
|
82
|
+
if existing_org_id:
|
83
|
+
print(f"Found existing ORGANIZATION_ID {'in keyring' if _KEYRING_AVAILABLE else 'in environment'}")
|
84
|
+
new_org_id = input("Enter new ORGANIZATION_ID (leave empty to keep existing): ")
|
85
|
+
org_id = new_org_id if new_org_id else existing_org_id
|
86
|
+
else:
|
87
|
+
org_id = input("Enter ORGANIZATION_ID: ")
|
88
|
+
if not org_id:
|
89
|
+
print("Error: ORGANIZATION_ID is required")
|
90
|
+
return False
|
91
|
+
|
92
|
+
# Ask for STATE_FILE or use existing
|
93
|
+
if existing_state_file:
|
94
|
+
print(f"Found existing STATE_FILE {'in keyring' if _KEYRING_AVAILABLE else 'in environment'}: {existing_state_file}")
|
95
|
+
new_state_file = input(f"Enter new STATE_FILE path (leave empty to keep existing, default: {suggested_state_file}): ")
|
96
|
+
state_file = new_state_file if new_state_file else existing_state_file
|
97
|
+
else:
|
98
|
+
state_file = input(f"Enter STATE_FILE path (default: {suggested_state_file}): ")
|
99
|
+
if not state_file:
|
100
|
+
state_file = suggested_state_file
|
101
|
+
print(f"Using default STATE_FILE path: {state_file}")
|
102
|
+
|
103
|
+
# Store in keyring
|
104
|
+
if _KEYRING_AVAILABLE:
|
105
|
+
if existing_token != token or not existing_token:
|
106
|
+
if set_to_keyring(_KEYRING_BWS_TOKEN_KEY, token):
|
107
|
+
print("BWS_TOKEN stored in keyring")
|
108
|
+
else:
|
109
|
+
print("Failed to store BWS_TOKEN in keyring")
|
110
|
+
return False
|
111
|
+
|
112
|
+
if existing_org_id != org_id or not existing_org_id:
|
113
|
+
if set_to_keyring(_KEYRING_ORG_ID_KEY, org_id):
|
114
|
+
print("ORGANIZATION_ID stored in keyring")
|
115
|
+
else:
|
116
|
+
print("Failed to store ORGANIZATION_ID in keyring")
|
117
|
+
return False
|
118
|
+
|
119
|
+
if existing_state_file != state_file or not existing_state_file:
|
120
|
+
if set_to_keyring(_KEYRING_STATE_FILE_KEY, state_file):
|
121
|
+
print("STATE_FILE stored in keyring")
|
122
|
+
else:
|
123
|
+
print("Failed to store STATE_FILE in keyring")
|
124
|
+
return False
|
125
|
+
else:
|
126
|
+
# Store in environment variables if keyring is not available
|
127
|
+
os.environ["BWS_TOKEN"] = token
|
128
|
+
os.environ["ORGANIZATION_ID"] = org_id
|
129
|
+
os.environ["STATE_FILE"] = state_file
|
130
|
+
print("Credentials stored in environment variables (not persistent)")
|
131
|
+
|
132
|
+
# Ensure state file directory exists
|
133
|
+
state_dir = os.path.dirname(state_file)
|
134
|
+
if state_dir and not os.path.exists(state_dir):
|
135
|
+
try:
|
136
|
+
os.makedirs(state_dir, exist_ok=True)
|
137
|
+
print(f"Created directory for STATE_FILE: {state_dir}")
|
138
|
+
except Exception as e:
|
139
|
+
print(f"Warning: Could not create state directory: {e}")
|
140
|
+
|
141
|
+
print("\nVault initialization completed successfully")
|
142
|
+
return True
|
143
|
+
|
144
|
+
|
145
|
+
def main():
|
146
|
+
"""
|
147
|
+
Main entry point for the command-line interface.
|
148
|
+
"""
|
149
|
+
parser = argparse.ArgumentParser(description="Bitwarden Secret Manager CLI")
|
150
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
151
|
+
|
152
|
+
# List command
|
153
|
+
list_parser = subparsers.add_parser("list", help="List projects")
|
154
|
+
list_parser.add_argument("--org-id", "-o", help="Organization ID")
|
155
|
+
|
156
|
+
# Init command
|
157
|
+
subparsers.add_parser("init", help="Initialize vault with BWS_TOKEN and ORGANIZATION_ID")
|
158
|
+
|
159
|
+
# Parse arguments
|
160
|
+
args = parser.parse_args()
|
161
|
+
|
162
|
+
# Execute command
|
163
|
+
if args.command == "list":
|
164
|
+
projects = list_projects(args.org_id)
|
165
|
+
if projects:
|
166
|
+
print("\nAvailable Projects:")
|
167
|
+
print("===================")
|
168
|
+
for project in projects:
|
169
|
+
print(f"ID: {project.id}")
|
170
|
+
print(f"Name: {project.name}")
|
171
|
+
print(f"Created: {project.creation_date}")
|
172
|
+
print("-" * 50)
|
173
|
+
else:
|
174
|
+
print("No projects found")
|
175
|
+
elif args.command == "init":
|
176
|
+
init_vault()
|
177
|
+
else:
|
178
|
+
parser.print_help()
|
179
|
+
|
180
|
+
|
181
|
+
if __name__ == "__main__":
|
182
|
+
main()
|
toru_vault/lazy_dict.py
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
from collections.abc import MutableMapping
|
2
|
+
from typing import Dict, Optional, Callable, Set, Iterator, Tuple
|
3
|
+
|
4
|
+
class LazySecretsDict(MutableMapping):
|
5
|
+
"""
|
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.
|
9
|
+
"""
|
10
|
+
|
11
|
+
def __init__(self,
|
12
|
+
secret_keys: Set[str],
|
13
|
+
getter_func: Callable[[str], str],
|
14
|
+
setter_func: Optional[Callable[[str, str], None]] = None,
|
15
|
+
deleter_func: Optional[Callable[[str], None]] = None):
|
16
|
+
"""
|
17
|
+
Initialize the lazy dictionary with a list of available keys and functions to retrieve/set/delete values.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
secret_keys: Set of keys that are available in this dictionary
|
21
|
+
getter_func: Function that takes a key and returns the secret value
|
22
|
+
setter_func: Optional function to set a value for a key
|
23
|
+
deleter_func: Optional function to delete a key
|
24
|
+
"""
|
25
|
+
self._keys = secret_keys
|
26
|
+
self._getter = getter_func
|
27
|
+
self._setter = setter_func
|
28
|
+
self._deleter = deleter_func
|
29
|
+
self._cache: Dict[str, str] = {}
|
30
|
+
|
31
|
+
def __getitem__(self, key: str) -> str:
|
32
|
+
"""Get an item from the dictionary, fetching/decrypting it on first access."""
|
33
|
+
if key not in self._keys:
|
34
|
+
raise KeyError(key)
|
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
|
42
|
+
|
43
|
+
return self._cache[key]
|
44
|
+
|
45
|
+
def __setitem__(self, key: str, value: str) -> None:
|
46
|
+
"""Set an item in the dictionary."""
|
47
|
+
if self._setter is None:
|
48
|
+
raise NotImplementedError("This dictionary does not support item assignment")
|
49
|
+
|
50
|
+
self._setter(key, value)
|
51
|
+
self._cache[key] = value
|
52
|
+
self._keys.add(key)
|
53
|
+
|
54
|
+
def __delitem__(self, key: str) -> None:
|
55
|
+
"""Delete an item from the dictionary."""
|
56
|
+
if self._deleter is None:
|
57
|
+
raise NotImplementedError("This dictionary does not support item deletion")
|
58
|
+
|
59
|
+
if key not in self._keys:
|
60
|
+
raise KeyError(key)
|
61
|
+
|
62
|
+
self._deleter(key)
|
63
|
+
if key in self._cache:
|
64
|
+
del self._cache[key]
|
65
|
+
self._keys.remove(key)
|
66
|
+
|
67
|
+
def __iter__(self) -> Iterator[str]:
|
68
|
+
"""Return an iterator over the keys."""
|
69
|
+
return iter(self._keys)
|
70
|
+
|
71
|
+
def __len__(self) -> int:
|
72
|
+
"""Return the number of keys."""
|
73
|
+
return len(self._keys)
|
74
|
+
|
75
|
+
def items(self) -> Iterator[Tuple[str, str]]:
|
76
|
+
"""Return an iterator over (key, value) pairs."""
|
77
|
+
for key in self._keys:
|
78
|
+
yield (key, self[key])
|
79
|
+
|
80
|
+
def keys(self) -> Set[str]:
|
81
|
+
"""Return the set of keys."""
|
82
|
+
return self._keys.copy()
|
83
|
+
|
84
|
+
def values(self) -> Iterator[str]:
|
85
|
+
"""Return an iterator over values."""
|
86
|
+
for key in self._keys:
|
87
|
+
yield self[key]
|
toru_vault/py.typed
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
toru_vault/vault.py
ADDED
@@ -0,0 +1,532 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import os
|
3
|
+
import logging
|
4
|
+
import time
|
5
|
+
import json
|
6
|
+
import tempfile
|
7
|
+
import stat
|
8
|
+
import atexit
|
9
|
+
import secrets as pysecrets
|
10
|
+
from typing import Dict, Optional, Tuple
|
11
|
+
from bitwarden_sdk import BitwardenClient, DeviceType, client_settings_from_dict
|
12
|
+
from cryptography.fernet import Fernet
|
13
|
+
from cryptography.hazmat.primitives import hashes
|
14
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
15
|
+
import base64
|
16
|
+
from .lazy_dict import LazySecretsDict
|
17
|
+
|
18
|
+
# Try importing keyring - it might not be available in container environments
|
19
|
+
try:
|
20
|
+
import keyring
|
21
|
+
_KEYRING_AVAILABLE = True
|
22
|
+
except ImportError:
|
23
|
+
_KEYRING_AVAILABLE = False
|
24
|
+
|
25
|
+
# Setup minimal logging
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
# Constants for keyring storage
|
29
|
+
_KEYRING_SERVICE_NAME = "bitwarden_vault"
|
30
|
+
_KEYRING_BWS_TOKEN_KEY = "bws_token"
|
31
|
+
_KEYRING_ORG_ID_KEY = "organization_id"
|
32
|
+
_KEYRING_STATE_FILE_KEY = "state_file"
|
33
|
+
|
34
|
+
# Secure cache configuration
|
35
|
+
_SECRET_CACHE_TIMEOUT = 300 # 5 minutes
|
36
|
+
_secrets_cache: Dict[str, Tuple[float, Dict[str, str]]] = {}
|
37
|
+
|
38
|
+
def _generate_encryption_key(salt: bytes = None) -> Tuple[bytes, bytes]:
|
39
|
+
"""
|
40
|
+
Generate an encryption key for securing the cache
|
41
|
+
|
42
|
+
Args:
|
43
|
+
salt (bytes, optional): Salt for key derivation
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Tuple[bytes, bytes]: Key and salt
|
47
|
+
"""
|
48
|
+
if salt is None:
|
49
|
+
salt = os.urandom(16)
|
50
|
+
|
51
|
+
# Generate a key from the machine-specific information and random salt
|
52
|
+
machine_id = _get_machine_id()
|
53
|
+
password = machine_id.encode()
|
54
|
+
|
55
|
+
kdf = PBKDF2HMAC(
|
56
|
+
algorithm=hashes.SHA256(),
|
57
|
+
length=32,
|
58
|
+
salt=salt,
|
59
|
+
iterations=100000,
|
60
|
+
)
|
61
|
+
key = base64.urlsafe_b64encode(kdf.derive(password))
|
62
|
+
return key, salt
|
63
|
+
|
64
|
+
def _get_machine_id() -> str:
|
65
|
+
"""Get a unique identifier for the current machine"""
|
66
|
+
# Try platform-specific methods to get a machine ID
|
67
|
+
machine_id = ""
|
68
|
+
|
69
|
+
if os.path.exists('/etc/machine-id'):
|
70
|
+
with open('/etc/machine-id', 'r') as f:
|
71
|
+
machine_id = f.read().strip()
|
72
|
+
elif os.path.exists('/var/lib/dbus/machine-id'):
|
73
|
+
with open('/var/lib/dbus/machine-id', 'r') as f:
|
74
|
+
machine_id = f.read().strip()
|
75
|
+
elif os.name == 'nt': # Windows
|
76
|
+
import subprocess
|
77
|
+
try:
|
78
|
+
result = subprocess.run(['wmic', 'csproduct', 'get', 'UUID'], capture_output=True, text=True)
|
79
|
+
if result.returncode == 0:
|
80
|
+
machine_id = result.stdout.strip().split('\n')[-1].strip()
|
81
|
+
except (FileNotFoundError, subprocess.SubprocessError):
|
82
|
+
pass
|
83
|
+
|
84
|
+
# Fallback if we couldn't get a machine ID
|
85
|
+
if not machine_id:
|
86
|
+
# Use a combination of hostname and a persisted random value
|
87
|
+
import socket
|
88
|
+
hostname = socket.gethostname()
|
89
|
+
|
90
|
+
# Create a persistent random ID
|
91
|
+
id_file = os.path.join(tempfile.gettempdir(), '.vault_machine_id')
|
92
|
+
if os.path.exists(id_file):
|
93
|
+
try:
|
94
|
+
with open(id_file, 'r') as f:
|
95
|
+
random_id = f.read().strip()
|
96
|
+
except Exception:
|
97
|
+
random_id = pysecrets.token_hex(16)
|
98
|
+
else:
|
99
|
+
random_id = pysecrets.token_hex(16)
|
100
|
+
try:
|
101
|
+
# Try to save it with restricted permissions
|
102
|
+
with open(id_file, 'w') as f:
|
103
|
+
f.write(random_id)
|
104
|
+
os.chmod(id_file, stat.S_IRUSR | stat.S_IWUSR) # 0600 permissions
|
105
|
+
except Exception:
|
106
|
+
pass
|
107
|
+
|
108
|
+
machine_id = f"{hostname}-{random_id}"
|
109
|
+
|
110
|
+
return machine_id
|
111
|
+
|
112
|
+
def _encrypt_secrets(secrets_dict: Dict[str, str]) -> Optional[str]:
|
113
|
+
"""
|
114
|
+
Encrypt secrets dictionary
|
115
|
+
|
116
|
+
Args:
|
117
|
+
secrets_dict (Dict[str, str]): Dictionary of secrets
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
Optional[str]: Encrypted data or None if encryption fails
|
121
|
+
"""
|
122
|
+
try:
|
123
|
+
key, salt = _generate_encryption_key()
|
124
|
+
if not key:
|
125
|
+
return None
|
126
|
+
|
127
|
+
# Encrypt the serialized secrets
|
128
|
+
f = Fernet(key)
|
129
|
+
encrypted_data = f.encrypt(json.dumps(secrets_dict).encode())
|
130
|
+
|
131
|
+
# Store along with the salt
|
132
|
+
return base64.urlsafe_b64encode(salt).decode() + ":" + encrypted_data.decode()
|
133
|
+
except Exception as e:
|
134
|
+
logger.warning(f"Failed to encrypt secrets: {e}")
|
135
|
+
return None
|
136
|
+
|
137
|
+
def _decrypt_secrets(encrypted_data: str) -> Optional[Dict[str, str]]:
|
138
|
+
"""
|
139
|
+
Decrypt secrets
|
140
|
+
|
141
|
+
Args:
|
142
|
+
encrypted_data (str): Encrypted data
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
Optional[Dict[str, str]]: Decrypted secrets dictionary or None if decryption fails
|
146
|
+
"""
|
147
|
+
try:
|
148
|
+
# Split salt and encrypted data
|
149
|
+
salt_b64, encrypted = encrypted_data.split(":", 1)
|
150
|
+
salt = base64.urlsafe_b64decode(salt_b64)
|
151
|
+
|
152
|
+
# Regenerate the key with the same salt
|
153
|
+
key, _ = _generate_encryption_key(salt)
|
154
|
+
if not key:
|
155
|
+
return None
|
156
|
+
|
157
|
+
# Decrypt the data
|
158
|
+
f = Fernet(key)
|
159
|
+
decrypted_data = f.decrypt(encrypted.encode())
|
160
|
+
|
161
|
+
return json.loads(decrypted_data.decode())
|
162
|
+
except Exception as e:
|
163
|
+
logger.warning(f"Failed to decrypt secrets: {e}")
|
164
|
+
return None
|
165
|
+
|
166
|
+
def _secure_state_file(state_path: str) -> None:
|
167
|
+
"""
|
168
|
+
Ensure the state file has secure permissions
|
169
|
+
|
170
|
+
Args:
|
171
|
+
state_path (str): Path to the state file
|
172
|
+
"""
|
173
|
+
try:
|
174
|
+
if os.path.exists(state_path):
|
175
|
+
if os.name == 'posix': # Linux/Mac
|
176
|
+
os.chmod(state_path, stat.S_IRUSR | stat.S_IWUSR) # 0600 permissions
|
177
|
+
elif os.name == 'nt': # Windows
|
178
|
+
import subprocess
|
179
|
+
subprocess.run(['icacls', state_path, '/inheritance:r', '/grant:r', f'{os.getlogin()}:(F)'],
|
180
|
+
capture_output=True)
|
181
|
+
except Exception as e:
|
182
|
+
logger.warning(f"Could not set secure permissions on state file: {e}")
|
183
|
+
|
184
|
+
def _clear_cache() -> None:
|
185
|
+
"""Clear the secrets cache on exit"""
|
186
|
+
global _secrets_cache
|
187
|
+
_secrets_cache = {}
|
188
|
+
|
189
|
+
# Register the cache clearing function to run on exit
|
190
|
+
atexit.register(_clear_cache)
|
191
|
+
|
192
|
+
def _get_from_keyring_or_env(key, env_var):
|
193
|
+
"""
|
194
|
+
Get a value from keyring or environment variable
|
195
|
+
|
196
|
+
Args:
|
197
|
+
key (str): Key in keyring
|
198
|
+
env_var (str): Environment variable name
|
199
|
+
|
200
|
+
Returns:
|
201
|
+
str: Value from keyring or environment variable
|
202
|
+
"""
|
203
|
+
value = None
|
204
|
+
|
205
|
+
# Try keyring first if available
|
206
|
+
if _KEYRING_AVAILABLE:
|
207
|
+
try:
|
208
|
+
value = keyring.get_password(_KEYRING_SERVICE_NAME, key)
|
209
|
+
except Exception as e:
|
210
|
+
logger.warning(f"Failed to get {key} from keyring: {e}")
|
211
|
+
|
212
|
+
# Fall back to environment variable
|
213
|
+
if not value:
|
214
|
+
value = os.getenv(env_var)
|
215
|
+
|
216
|
+
return value
|
217
|
+
|
218
|
+
def set_to_keyring(key, value):
|
219
|
+
"""
|
220
|
+
Set a value to keyring
|
221
|
+
|
222
|
+
Args:
|
223
|
+
key (str): Key in keyring
|
224
|
+
value (str): Value to store
|
225
|
+
|
226
|
+
Returns:
|
227
|
+
bool: True if successful, False otherwise
|
228
|
+
"""
|
229
|
+
if not _KEYRING_AVAILABLE:
|
230
|
+
return False
|
231
|
+
|
232
|
+
try:
|
233
|
+
keyring.set_password(_KEYRING_SERVICE_NAME, key, value)
|
234
|
+
return True
|
235
|
+
except Exception as e:
|
236
|
+
logger.warning(f"Failed to set {key} to keyring: {e}")
|
237
|
+
return False
|
238
|
+
|
239
|
+
def _initialize_client():
|
240
|
+
"""
|
241
|
+
Initialize the Bitwarden client
|
242
|
+
"""
|
243
|
+
# Get environment variables with defaults
|
244
|
+
api_url = os.getenv("API_URL", "https://api.bitwarden.com")
|
245
|
+
identity_url = os.getenv("IDENTITY_URL", "https://identity.bitwarden.com")
|
246
|
+
|
247
|
+
# Get BWS_TOKEN from keyring or environment variable
|
248
|
+
bws_token = _get_from_keyring_or_env(_KEYRING_BWS_TOKEN_KEY, "BWS_TOKEN")
|
249
|
+
|
250
|
+
# Get STATE_FILE from keyring or environment variable
|
251
|
+
state_path = _get_from_keyring_or_env(_KEYRING_STATE_FILE_KEY, "STATE_FILE")
|
252
|
+
|
253
|
+
# Validate required environment variables
|
254
|
+
if not bws_token:
|
255
|
+
raise ValueError("BWS_TOKEN not found in keyring or environment variable")
|
256
|
+
if not state_path:
|
257
|
+
raise ValueError("STATE_FILE not found in keyring or environment variable")
|
258
|
+
|
259
|
+
# Ensure state file directory exists
|
260
|
+
state_dir = os.path.dirname(state_path)
|
261
|
+
if state_dir and not os.path.exists(state_dir):
|
262
|
+
try:
|
263
|
+
os.makedirs(state_dir, exist_ok=True)
|
264
|
+
# Secure the directory if possible
|
265
|
+
if os.name == 'posix': # Linux/Mac
|
266
|
+
os.chmod(state_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) # 0700 permissions
|
267
|
+
except Exception as e:
|
268
|
+
logger.warning(f"Could not create state directory with secure permissions: {e}")
|
269
|
+
|
270
|
+
# Secure the state file
|
271
|
+
_secure_state_file(state_path)
|
272
|
+
|
273
|
+
# Create and initialize the client
|
274
|
+
client = BitwardenClient(
|
275
|
+
client_settings_from_dict({
|
276
|
+
"apiUrl": api_url,
|
277
|
+
"deviceType": DeviceType.SDK,
|
278
|
+
"identityUrl": identity_url,
|
279
|
+
"userAgent": "Python",
|
280
|
+
})
|
281
|
+
)
|
282
|
+
|
283
|
+
# Authenticate with the Secrets Manager Access Token
|
284
|
+
client.auth().login_access_token(bws_token, state_path)
|
285
|
+
|
286
|
+
return client
|
287
|
+
|
288
|
+
def _load_secrets(project_id=None):
|
289
|
+
"""
|
290
|
+
Load secrets from Bitwarden
|
291
|
+
|
292
|
+
Args:
|
293
|
+
project_id (str): Project ID to filter secrets
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
dict: Dictionary of secrets with their names as keys
|
297
|
+
"""
|
298
|
+
# Initialize client with credentials from environment or keyring
|
299
|
+
try:
|
300
|
+
client = _initialize_client()
|
301
|
+
except Exception as e:
|
302
|
+
logger.error(f"Failed to initialize Bitwarden client: {e}")
|
303
|
+
return {}
|
304
|
+
|
305
|
+
# Get ORGANIZATION_ID from keyring or environment variable
|
306
|
+
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
307
|
+
if not organization_id:
|
308
|
+
logger.error("ORGANIZATION_ID not found in keyring or environment variable")
|
309
|
+
return {}
|
310
|
+
|
311
|
+
# Get secrets from BitWarden
|
312
|
+
try:
|
313
|
+
# Sync secrets to ensure we have the latest
|
314
|
+
client.secrets().sync(organization_id, None)
|
315
|
+
|
316
|
+
# Initialize empty secrets dictionary
|
317
|
+
secrets = {}
|
318
|
+
|
319
|
+
# Retrieve all secrets
|
320
|
+
all_secrets = client.secrets().list(organization_id)
|
321
|
+
|
322
|
+
# Validate response format
|
323
|
+
if not hasattr(all_secrets, 'data') or not hasattr(all_secrets.data, 'data'):
|
324
|
+
return {}
|
325
|
+
|
326
|
+
# We need to collect all secret IDs first
|
327
|
+
secret_ids = []
|
328
|
+
for secret in all_secrets.data.data:
|
329
|
+
secret_ids.append(secret.id)
|
330
|
+
|
331
|
+
# If we have secret IDs, fetch their values
|
332
|
+
if secret_ids:
|
333
|
+
# Get detailed information for all secrets by their IDs
|
334
|
+
secrets_detailed = client.secrets().get_by_ids(secret_ids)
|
335
|
+
|
336
|
+
# Validate response format
|
337
|
+
if not hasattr(secrets_detailed, 'data') or not hasattr(secrets_detailed.data, 'data'):
|
338
|
+
return {}
|
339
|
+
|
340
|
+
# Process each secret
|
341
|
+
for secret in secrets_detailed.data.data:
|
342
|
+
# Extract the project ID
|
343
|
+
secret_project_id = getattr(secret, 'project_id', None)
|
344
|
+
|
345
|
+
# Check if this secret belongs to the specified project
|
346
|
+
if project_id and secret_project_id is not None and project_id != str(secret_project_id):
|
347
|
+
continue
|
348
|
+
|
349
|
+
# Add the secret to our dictionary
|
350
|
+
secrets[secret.key] = secret.value
|
351
|
+
|
352
|
+
# Update the cache with encryption
|
353
|
+
encrypted_data = _encrypt_secrets(secrets)
|
354
|
+
if encrypted_data:
|
355
|
+
_secrets_cache[f"{organization_id}:{project_id or ''}"] = (time.time(), encrypted_data)
|
356
|
+
else:
|
357
|
+
_secrets_cache[f"{organization_id}:{project_id or ''}"] = (time.time(), secrets.copy())
|
358
|
+
|
359
|
+
return secrets
|
360
|
+
except Exception as e:
|
361
|
+
logger.error(f"Error loading secrets: {e}")
|
362
|
+
raise
|
363
|
+
|
364
|
+
def env_load(project_id=None, override=False):
|
365
|
+
"""
|
366
|
+
Load all secrets related to the project into environmental variables.
|
367
|
+
|
368
|
+
Args:
|
369
|
+
project_id (str, optional): Project ID to filter secrets
|
370
|
+
override (bool, optional): Whether to override existing environment variables
|
371
|
+
"""
|
372
|
+
# Get all secrets from BWS
|
373
|
+
secrets = _load_secrets(project_id)
|
374
|
+
|
375
|
+
# Set environment variables
|
376
|
+
for key, value in secrets.items():
|
377
|
+
if override or key not in os.environ:
|
378
|
+
os.environ[key] = value
|
379
|
+
|
380
|
+
def env_load_all(override=False):
|
381
|
+
"""
|
382
|
+
Load all secrets from all projects that user has access to into environment variables
|
383
|
+
|
384
|
+
Args:
|
385
|
+
override (bool, optional): Whether to override existing environment variables
|
386
|
+
"""
|
387
|
+
# Get ORGANIZATION_ID from keyring or environment variable
|
388
|
+
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
389
|
+
|
390
|
+
# Initialize Bitwarden client
|
391
|
+
try:
|
392
|
+
client = _initialize_client()
|
393
|
+
except Exception as e:
|
394
|
+
logger.error(f"Failed to initialize Bitwarden client: {e}")
|
395
|
+
return
|
396
|
+
|
397
|
+
try:
|
398
|
+
# Sync to ensure we have the latest data
|
399
|
+
client.secrets().sync(organization_id, None)
|
400
|
+
|
401
|
+
# Get all projects
|
402
|
+
projects_response = client.projects().list(organization_id)
|
403
|
+
|
404
|
+
# Validate response format
|
405
|
+
if not hasattr(projects_response, 'data') or not hasattr(projects_response.data, 'data'):
|
406
|
+
logger.warning(f"No projects found in organization {organization_id}")
|
407
|
+
return
|
408
|
+
|
409
|
+
# Process each project
|
410
|
+
for project in projects_response.data.data:
|
411
|
+
if hasattr(project, 'id'):
|
412
|
+
project_id = str(project.id)
|
413
|
+
|
414
|
+
# Load environment variables for this project
|
415
|
+
try:
|
416
|
+
# Get the secrets for this project and set them as environment variables
|
417
|
+
env_load(project_id=project_id, override=override)
|
418
|
+
logger.info(f"Loaded secrets from project: {getattr(project, 'name', project_id)}")
|
419
|
+
except Exception as e:
|
420
|
+
logger.warning(f"Failed to load secrets from project {project_id}: {e}")
|
421
|
+
|
422
|
+
except Exception as e:
|
423
|
+
logger.error(f"Failed to load all secrets into environment variables: {e}")
|
424
|
+
|
425
|
+
def get(project_id=None, refresh=False, use_keyring=True):
|
426
|
+
"""
|
427
|
+
Return a dictionary of all project secrets
|
428
|
+
|
429
|
+
Args:
|
430
|
+
project_id (str, optional): Project ID to filter secrets
|
431
|
+
refresh (bool, optional): Force refresh the secrets cache
|
432
|
+
use_keyring (bool, optional): Whether to use system keyring (True) or in-memory encryption (False)
|
433
|
+
|
434
|
+
Returns:
|
435
|
+
dict: Dictionary of secrets with their names as keys, using lazy loading
|
436
|
+
"""
|
437
|
+
# Get ORGANIZATION_ID from keyring or environment variable
|
438
|
+
organization_id = _get_from_keyring_or_env(_KEYRING_ORG_ID_KEY, "ORGANIZATION_ID")
|
439
|
+
|
440
|
+
# Build the service name for keyring storage
|
441
|
+
service_name = f"vault_{organization_id or 'default'}"
|
442
|
+
|
443
|
+
# Function to either fetch from keyring or decrypt from cache based on container flag
|
444
|
+
def _load_decrypted_secrets():
|
445
|
+
# Check if we need to force a refresh
|
446
|
+
if refresh:
|
447
|
+
return _load_secrets(project_id)
|
448
|
+
|
449
|
+
# Otherwise try to use cached values first
|
450
|
+
cache_key = f"{organization_id}:{project_id or ''}"
|
451
|
+
current_time = time.time()
|
452
|
+
|
453
|
+
if cache_key in _secrets_cache:
|
454
|
+
timestamp, encrypted_secrets = _secrets_cache[cache_key]
|
455
|
+
|
456
|
+
# If cache hasn't expired
|
457
|
+
if current_time - timestamp < _SECRET_CACHE_TIMEOUT:
|
458
|
+
# If we have encryption, try to decrypt
|
459
|
+
if encrypted_secrets:
|
460
|
+
decrypted_secrets = _decrypt_secrets(encrypted_secrets)
|
461
|
+
if decrypted_secrets:
|
462
|
+
return decrypted_secrets
|
463
|
+
# Otherwise return the unencrypted data (backward compatibility)
|
464
|
+
elif isinstance(encrypted_secrets, dict):
|
465
|
+
return encrypted_secrets.copy()
|
466
|
+
|
467
|
+
# If we couldn't get from cache, load fresh
|
468
|
+
return _load_secrets(project_id)
|
469
|
+
|
470
|
+
# Get all secrets and their keys
|
471
|
+
all_secrets = _load_decrypted_secrets()
|
472
|
+
secret_keys = set(all_secrets.keys())
|
473
|
+
|
474
|
+
# Store secrets in keyring if available and requested
|
475
|
+
keyring_usable = _KEYRING_AVAILABLE and use_keyring
|
476
|
+
if keyring_usable:
|
477
|
+
for key, value in all_secrets.items():
|
478
|
+
keyring.set_password(service_name, key, value)
|
479
|
+
|
480
|
+
# When keyring is unavailable or not requested (likely in container)
|
481
|
+
if not keyring_usable:
|
482
|
+
# Create a dictionary of cached secrets for container mode
|
483
|
+
container_secrets = {}
|
484
|
+
encrypted_data = None
|
485
|
+
cache_key = f"{organization_id}:{project_id or ''}"
|
486
|
+
|
487
|
+
# If we have a cached encrypted version, use that
|
488
|
+
if cache_key in _secrets_cache:
|
489
|
+
_, encrypted_data = _secrets_cache[cache_key]
|
490
|
+
|
491
|
+
# Create getter function for container mode
|
492
|
+
def _container_getter(key):
|
493
|
+
if key in container_secrets:
|
494
|
+
return container_secrets[key]
|
495
|
+
|
496
|
+
# If not in memory cache, check if we have pre-loaded decrypted secrets
|
497
|
+
if all_secrets and key in all_secrets:
|
498
|
+
container_secrets[key] = all_secrets[key]
|
499
|
+
return container_secrets[key]
|
500
|
+
|
501
|
+
# Otherwise, try to decrypt from cache
|
502
|
+
if encrypted_data and not isinstance(encrypted_data, dict):
|
503
|
+
decrypted = _decrypt_secrets(encrypted_data)
|
504
|
+
if decrypted and key in decrypted:
|
505
|
+
container_secrets[key] = decrypted[key]
|
506
|
+
return container_secrets[key]
|
507
|
+
|
508
|
+
# If all else fails, load from API
|
509
|
+
fresh_secrets = _load_secrets(project_id)
|
510
|
+
if key in fresh_secrets:
|
511
|
+
container_secrets[key] = fresh_secrets[key]
|
512
|
+
return container_secrets[key]
|
513
|
+
|
514
|
+
return None
|
515
|
+
|
516
|
+
# Create the lazy dictionary with container getter
|
517
|
+
return LazySecretsDict(secret_keys, _container_getter)
|
518
|
+
else:
|
519
|
+
# Create getter function for keyring mode
|
520
|
+
def _keyring_getter(key):
|
521
|
+
return keyring.get_password(service_name, key)
|
522
|
+
|
523
|
+
# Create setter function for keyring mode
|
524
|
+
def _keyring_setter(key, value):
|
525
|
+
keyring.set_password(service_name, key, value)
|
526
|
+
|
527
|
+
# Create deleter function for keyring mode
|
528
|
+
def _keyring_deleter(key):
|
529
|
+
keyring.delete_password(service_name, key)
|
530
|
+
|
531
|
+
# Create the lazy dictionary with keyring getter/setter/deleter
|
532
|
+
return LazySecretsDict(secret_keys, _keyring_getter, _keyring_setter, _keyring_deleter)
|
@@ -0,0 +1,250 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: toru-vault
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: ToruVault: A simple Python package for managing Bitwarden secrets
|
5
|
+
Author: Toru AI
|
6
|
+
Author-email: ToruAI <mpaszynski@toruai.com>
|
7
|
+
License: MIT
|
8
|
+
Project-URL: Homepage, https://github.com/ToruAI/vault
|
9
|
+
Project-URL: Issues, https://github.com/ToruAI/vault/issues
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Requires-Python: >=3.6
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
License-File: LICENSE
|
16
|
+
Requires-Dist: bitwarden-sdk
|
17
|
+
Requires-Dist: cryptography>=36.0.0
|
18
|
+
Dynamic: author
|
19
|
+
Dynamic: license-file
|
20
|
+
Dynamic: requires-python
|
21
|
+
|
22
|
+
<p align="center">
|
23
|
+
<img src="img/logo.svg" alt="ToruVault Logo" width="300"/>
|
24
|
+
</p>
|
25
|
+
|
26
|
+
# ToruVault
|
27
|
+
|
28
|
+
A simple Python package for managing Bitwarden secrets with enhanced security.
|
29
|
+
|
30
|
+
|
31
|
+

|
32
|
+

|
33
|
+

|
34
|
+
|
35
|
+
## Features
|
36
|
+
|
37
|
+
- Load secrets from Bitwarden Secret Manager into environment variables
|
38
|
+
- Get secrets as a Python dictionary
|
39
|
+
- Filter secrets by project ID
|
40
|
+
- Secure in-memory caching with encryption
|
41
|
+
- Automatic cache expiration (5 minutes)
|
42
|
+
- Secure file permissions for state storage
|
43
|
+
- Machine-specific secret protection
|
44
|
+
- Secure credential storage using OS keyring
|
45
|
+
|
46
|
+
## Installation
|
47
|
+
|
48
|
+
### Using UV (Recommended)
|
49
|
+
|
50
|
+
```bash
|
51
|
+
# Install UV if you don't have it already
|
52
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
53
|
+
|
54
|
+
# Install toru-vault package
|
55
|
+
uv pip install toru-vault
|
56
|
+
|
57
|
+
# Or install in a virtual environment (recommended)
|
58
|
+
uv venv create -p python3.10 .venv
|
59
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
60
|
+
uv pip install toru-vault
|
61
|
+
```
|
62
|
+
|
63
|
+
|
64
|
+
This will automatically install all required dependencies:
|
65
|
+
- bitwarden-sdk - For interfacing with Bitwarden API
|
66
|
+
- keyring - For secure credential storage
|
67
|
+
- cryptography - For encryption/decryption operations
|
68
|
+
|
69
|
+
### From Source with UV
|
70
|
+
|
71
|
+
```bash
|
72
|
+
# Clone the repository
|
73
|
+
git clone https://github.com/ToruAI/vault.git
|
74
|
+
cd vault
|
75
|
+
|
76
|
+
uv venv create -p python3.10 .venv
|
77
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
78
|
+
|
79
|
+
# Install dependencies
|
80
|
+
uv pip install -r requirements.txt
|
81
|
+
|
82
|
+
# Install in development mode
|
83
|
+
uv pip install -e .
|
84
|
+
```
|
85
|
+
|
86
|
+
## Configuration
|
87
|
+
|
88
|
+
You have two options for configuring the vault:
|
89
|
+
|
90
|
+
### Option 1: Initialize with Keyring Storage (Recommended)
|
91
|
+
|
92
|
+
The most secure way to set up vault is to use your operating system's secure keyring:
|
93
|
+
|
94
|
+
```bash
|
95
|
+
# Initialize vault with secure keyring storage
|
96
|
+
python -m vault init
|
97
|
+
```
|
98
|
+
|
99
|
+
This will prompt you to enter:
|
100
|
+
- Your Bitwarden access token (BWS_TOKEN)
|
101
|
+
- Your Bitwarden organization ID (ORGANIZATION_ID)
|
102
|
+
- The path to the state file (STATE_FILE)
|
103
|
+
|
104
|
+
[How to get the BWS_TOKEN, ORGANIZATION_ID, and STATE_FILE](#Bitwarden-Secrets)
|
105
|
+
|
106
|
+
These credentials will be securely stored in your OS keyring and used automatically by the vault.
|
107
|
+
|
108
|
+
### Option 2: Environment Variables
|
109
|
+
|
110
|
+
Alternatively, you can set the following environment variables:
|
111
|
+
|
112
|
+
- `BWS_TOKEN`: Your Bitwarden access token
|
113
|
+
- `ORGANIZATION_ID`: Your Bitwarden organization ID
|
114
|
+
- `STATE_FILE`: Path to the state file (must be in an existing directory)
|
115
|
+
- `API_URL` (optional): Defaults to "https://api.bitwarden.com"
|
116
|
+
- `IDENTITY_URL` (optional): Defaults to "https://identity.bitwarden.com"
|
117
|
+
|
118
|
+
Setting these environment variables is useful for container environments or when keyring is not available.
|
119
|
+
|
120
|
+
## CLI Commands
|
121
|
+
|
122
|
+
### Initialize Vault
|
123
|
+
|
124
|
+
```bash
|
125
|
+
# Set up vault with secure credential storage
|
126
|
+
python -m vault init
|
127
|
+
```
|
128
|
+
|
129
|
+
### Listing Available Projects
|
130
|
+
|
131
|
+
```bash
|
132
|
+
# List all projects in your organization
|
133
|
+
python -m vault list
|
134
|
+
|
135
|
+
# With a specific organization ID
|
136
|
+
python -m vault list --org-id YOUR_ORGANIZATION_ID
|
137
|
+
```
|
138
|
+
|
139
|
+
## Python Usage
|
140
|
+
|
141
|
+
### Loading secrets into environment variables
|
142
|
+
|
143
|
+
```python
|
144
|
+
import toru_vault as vault
|
145
|
+
|
146
|
+
# Load all secrets into environment variables
|
147
|
+
vault.env_load()
|
148
|
+
|
149
|
+
# Now you can access secrets as environment variables
|
150
|
+
import os
|
151
|
+
print(os.environ.get("SECRET_NAME"))
|
152
|
+
|
153
|
+
# Load secrets for a specific project
|
154
|
+
vault.env_load(project_id="your-project-id")
|
155
|
+
|
156
|
+
# Override existing environment variables (default: False)
|
157
|
+
vault.env_load(override=True)
|
158
|
+
```
|
159
|
+
|
160
|
+
### Getting secrets as a dictionary
|
161
|
+
|
162
|
+
```python
|
163
|
+
import toru_vault as vault
|
164
|
+
|
165
|
+
# Get all secrets as a dictionary
|
166
|
+
secrets = vault.get()
|
167
|
+
print(secrets["SECRET_NAME"]) # Secret is only decrypted when accessed
|
168
|
+
|
169
|
+
# Force refresh the cache
|
170
|
+
secrets = vault.get(refresh=True)
|
171
|
+
|
172
|
+
# Get secrets for a specific project
|
173
|
+
secrets = vault.get(project_id="your-project-id")
|
174
|
+
|
175
|
+
# Use in-memory encryption instead of system keyring
|
176
|
+
secrets = vault.get(use_keyring=False)
|
177
|
+
```
|
178
|
+
|
179
|
+
### Loading secrets from all projects
|
180
|
+
|
181
|
+
```python
|
182
|
+
import toru_vault as vault
|
183
|
+
|
184
|
+
# Load secrets from all projects you have access to into environment variables
|
185
|
+
vault.env_load_all()
|
186
|
+
|
187
|
+
# Override existing environment variables (default: False)
|
188
|
+
vault.env_load_all(override=True)
|
189
|
+
```
|
190
|
+
|
191
|
+
## Security Features
|
192
|
+
|
193
|
+
The vault package includes several security enhancements:
|
194
|
+
|
195
|
+
1. **OS Keyring Integration**: Securely stores BWS_TOKEN, ORGANIZATION_ID, and STATE_FILE in your OS keyring
|
196
|
+
2. **Memory Protection**: Secrets are encrypted in memory using Fernet encryption (AES-128)
|
197
|
+
3. **Lazy Decryption**: Secrets are only decrypted when explicitly accessed
|
198
|
+
4. **Cache Expiration**: Cached secrets expire after 5 minutes by default
|
199
|
+
5. **Secure File Permissions**: Sets secure permissions on state files
|
200
|
+
6. **Machine-Specific Encryption**: Uses machine-specific identifiers for encryption keys
|
201
|
+
7. **Cache Clearing**: Automatically clears secret cache on program exit
|
202
|
+
8. **Environment Variable Protection**: Doesn't override existing environment variables by default
|
203
|
+
9. **Secure Key Derivation**: Uses PBKDF2 with SHA-256 for key derivation
|
204
|
+
10. **No Direct Storage**: Never stores secrets in plain text on disk
|
205
|
+
|
206
|
+
## Bitwarden Secrets
|
207
|
+
|
208
|
+
### BWS_TOKEN
|
209
|
+
|
210
|
+
Your Bitwarden access token. You can get it from the Bitwarden web app:
|
211
|
+
|
212
|
+
1. Log in to your Bitwarden account
|
213
|
+
2. Go to Secret Manager at left bottom
|
214
|
+
3. Go to the "Machine accounts" section
|
215
|
+
4. Create new machine account.
|
216
|
+
5. Go to Access Token Tab
|
217
|
+

|
218
|
+
6. This is your `BWS_TOKEN`.
|
219
|
+
|
220
|
+
Remember that you need to assign access to the machine account for the projects you want to use.
|
221
|
+
|
222
|
+
### ORGANIZATION_ID
|
223
|
+
|
224
|
+
Your Bitwarden organization ID. You can get it from the Bitwarden web app:
|
225
|
+
|
226
|
+
1. Log in to your Bitwarden account
|
227
|
+
2. Go to Secret Manager at left bottom
|
228
|
+
3. Go to the "Machine accounts" section
|
229
|
+
4. Create new machine account.
|
230
|
+
5. Go to Config Tab
|
231
|
+
6. There is your `ORGANIZATION_ID`.
|
232
|
+
|
233
|
+
### STATE_FILE
|
234
|
+
|
235
|
+
The `STATE_FILE` is used by the login_access_token method to store persistent authentication state information after successfully logging in with an access token.
|
236
|
+
|
237
|
+
You can set it to any existing file path.
|
238
|
+
|
239
|
+
## Security Best Practices
|
240
|
+
|
241
|
+
When working with secrets, always follow these important guidelines:
|
242
|
+
|
243
|
+
1. **Never Embed Keys in Code**: Always use environment variables, keyring, or secure secret management systems.
|
244
|
+
2. **Never Commit Secrets**: Add secret files and credentials to your `.gitignore` file.
|
245
|
+
3. **Use Key Rotation**: Regularly rotate your access tokens as a security measure.
|
246
|
+
4. **Limit Access**: Only provide access to secrets on a need-to-know basis.
|
247
|
+
5. **Monitor Usage**: Regularly audit which applications and users are accessing your secrets.
|
248
|
+
6. **Use Environment-Specific Secrets**: Use different secrets for development, staging, and production environments.
|
249
|
+
|
250
|
+
Remember that the vault package is designed to protect secrets once they're in your system, but you must handle the initial configuration securely.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
toru_vault/__init__.py,sha256=Co9SSa9gFFTME0YcMzA1vEqJxs045-0kYfdP9GxGasU,177
|
2
|
+
toru_vault/__main__.py,sha256=KRw1dF3tK71DDmAac30tbBgSBRCNyCLOe1NylNXxRi4,6702
|
3
|
+
toru_vault/lazy_dict.py,sha256=OZVD-VYQHFRzMw1dOPXpagnddAJNNCZKtcdmTiio4lk,3232
|
4
|
+
toru_vault/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
5
|
+
toru_vault/vault.py,sha256=RLJanRaVnOb4SCvKjJiZJYlUX2QBHRzqpI-Z_vp32Jk,18981
|
6
|
+
toru_vault-0.1.0.dist-info/licenses/LICENSE,sha256=TbuuchABSutbmmaI1M232F22GsaI88_hwEvto5w_Ux4,1063
|
7
|
+
toru_vault-0.1.0.dist-info/METADATA,sha256=FJ5_wb8Wj5zgnBPwtTT0440UAtxWffymw2s1MGHhxhM,7665
|
8
|
+
toru_vault-0.1.0.dist-info/WHEEL,sha256=A8Eltl-h0W-qZDVezsLjjslosEH_pdYC2lQ0JcbgCzs,91
|
9
|
+
toru_vault-0.1.0.dist-info/entry_points.txt,sha256=dfqkbNftpmAv0iKzVgdkjymkCfj3TwzUrQm2PO7Xgxs,56
|
10
|
+
toru_vault-0.1.0.dist-info/top_level.txt,sha256=c9ulQ18kKs3HbkI5oeoLmnFTknjC0rY1BwsNLJKDua8,11
|
11
|
+
toru_vault-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 ToruAI
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
toru_vault
|