vaultedge 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vaultedge-1.0.0/PKG-INFO +25 -0
- vaultedge-1.0.0/pyproject.toml +38 -0
- vaultedge-1.0.0/setup.cfg +4 -0
- vaultedge-1.0.0/vaultedge/__init__.py +15 -0
- vaultedge-1.0.0/vaultedge/client.py +271 -0
- vaultedge-1.0.0/vaultedge/vault.py +107 -0
- vaultedge-1.0.0/vaultedge.egg-info/PKG-INFO +25 -0
- vaultedge-1.0.0/vaultedge.egg-info/SOURCES.txt +9 -0
- vaultedge-1.0.0/vaultedge.egg-info/dependency_links.txt +1 -0
- vaultedge-1.0.0/vaultedge.egg-info/requires.txt +6 -0
- vaultedge-1.0.0/vaultedge.egg-info/top_level.txt +1 -0
vaultedge-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vaultedge
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: VaultEdge — zero-trust AI key manager SDK for Python
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://vaultedge.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/you/vaultedge
|
|
8
|
+
Keywords: ai,llm,api-key,vault,openai,security,proxy
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security :: Cryptography
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: cryptography>=42.0.0
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vaultedge"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "VaultEdge — zero-trust AI key manager SDK for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["ai", "llm", "api-key", "vault", "openai", "security", "proxy"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Security :: Cryptography",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"cryptography>=42.0.0",
|
|
27
|
+
"httpx>=0.27.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://vaultedge.dev"
|
|
35
|
+
Repository = "https://github.com/you/vaultedge"
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""VaultEdge — zero-trust AI key manager SDK for Python."""
|
|
2
|
+
|
|
3
|
+
from .vault import VaultEntry, encrypt_vault, decrypt_vault, VAULT_PREFIX
|
|
4
|
+
from .client import VaultEdge, resolve_provider
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"VaultEdge",
|
|
8
|
+
"VaultEntry",
|
|
9
|
+
"encrypt_vault",
|
|
10
|
+
"decrypt_vault",
|
|
11
|
+
"resolve_provider",
|
|
12
|
+
"VAULT_PREFIX",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VaultEdge Python SDK — main client.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from vaultedge import VaultEdge
|
|
8
|
+
|
|
9
|
+
ve = VaultEdge(
|
|
10
|
+
vault=os.environ["VAULTEDGE_VAULT"],
|
|
11
|
+
password=os.environ["VAULTEDGE_PASSWORD"],
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
async def main():
|
|
15
|
+
response = await ve.chat.completions.create(
|
|
16
|
+
model="gpt-4o",
|
|
17
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
18
|
+
)
|
|
19
|
+
print(response["choices"][0]["message"]["content"])
|
|
20
|
+
|
|
21
|
+
asyncio.run(main())
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, Union
|
|
26
|
+
import httpx
|
|
27
|
+
|
|
28
|
+
from .vault import VaultEntry, decrypt_vault
|
|
29
|
+
|
|
30
|
+
# ─── Provider Config ───────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
PROVIDER_CONFIGS: Dict[str, Dict[str, Any]] = {
|
|
33
|
+
"OpenAI": {"url": "https://api.openai.com/v1/chat/completions", "scheme": "bearer"},
|
|
34
|
+
"Groq": {"url": "https://api.groq.com/openai/v1/chat/completions", "scheme": "bearer"},
|
|
35
|
+
"Anthropic": {"url": "https://api.anthropic.com/v1/messages", "scheme": "x-api-key"},
|
|
36
|
+
"Gemini": {"url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", "scheme": "bearer"},
|
|
37
|
+
"Mistral": {"url": "https://api.mistral.ai/v1/chat/completions", "scheme": "bearer"},
|
|
38
|
+
"xAI": {"url": "https://api.x.ai/v1/chat/completions", "scheme": "bearer"},
|
|
39
|
+
"DeepSeek": {"url": "https://api.deepseek.com/v1/chat/completions", "scheme": "bearer"},
|
|
40
|
+
"OpenRouter": {"url": "https://openrouter.ai/api/v1/chat/completions", "scheme": "bearer"},
|
|
41
|
+
"Cohere": {"url": "https://api.cohere.ai/v1/chat/completions", "scheme": "bearer"},
|
|
42
|
+
"Cerebras": {"url": "https://api.cerebras.ai/v1/chat/completions", "scheme": "bearer"},
|
|
43
|
+
"Sambanova": {"url": "https://api.sambanova.ai/v1/chat/completions", "scheme": "bearer"},
|
|
44
|
+
"Nvidia": {"url": "https://integrate.api.nvidia.com/v1/chat/completions", "scheme": "bearer"},
|
|
45
|
+
"Together": {"url": "https://api.together.xyz/v1/chat/completions", "scheme": "bearer"},
|
|
46
|
+
"Perplexity": {"url": "https://api.perplexity.ai/chat/completions", "scheme": "bearer"},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
MODEL_PREFIX_MAP = [
|
|
50
|
+
("gpt-", "OpenAI"),
|
|
51
|
+
("o1", "OpenAI"),
|
|
52
|
+
("o3", "OpenAI"),
|
|
53
|
+
("davinci", "OpenAI"),
|
|
54
|
+
("llama", "Groq"),
|
|
55
|
+
("groq", "Groq"),
|
|
56
|
+
("mixtral", "Groq"),
|
|
57
|
+
("gemma", "Groq"),
|
|
58
|
+
("gemini", "Gemini"),
|
|
59
|
+
("claude", "Anthropic"),
|
|
60
|
+
("mistral", "Mistral"),
|
|
61
|
+
("codestral", "Mistral"),
|
|
62
|
+
("grok", "xAI"),
|
|
63
|
+
("deepseek", "DeepSeek"),
|
|
64
|
+
("command", "Cohere"),
|
|
65
|
+
("cohere", "Cohere"),
|
|
66
|
+
("nvidia", "Nvidia"),
|
|
67
|
+
("nim", "Nvidia"),
|
|
68
|
+
("sonar", "Perplexity"),
|
|
69
|
+
("pplx-", "Perplexity"),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_provider(model: str) -> Optional[str]:
|
|
74
|
+
lower = model.lower()
|
|
75
|
+
for prefix, provider in MODEL_PREFIX_MAP:
|
|
76
|
+
if lower.startswith(prefix):
|
|
77
|
+
return provider
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_headers(provider: str, api_key: str) -> Dict[str, str]:
|
|
82
|
+
config = PROVIDER_CONFIGS.get(provider, {})
|
|
83
|
+
scheme = config.get("scheme", "bearer")
|
|
84
|
+
headers = {"Content-Type": "application/json"}
|
|
85
|
+
if scheme == "bearer":
|
|
86
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
87
|
+
elif scheme == "x-api-key":
|
|
88
|
+
headers["x-api-key"] = api_key
|
|
89
|
+
headers["anthropic-version"] = "2023-06-01"
|
|
90
|
+
return headers
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ─── Completions ───────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
class Completions:
|
|
96
|
+
def __init__(self, client: "VaultEdge") -> None:
|
|
97
|
+
self._client = client
|
|
98
|
+
|
|
99
|
+
async def create(
|
|
100
|
+
self,
|
|
101
|
+
model: str,
|
|
102
|
+
messages: List[Dict[str, Any]],
|
|
103
|
+
*,
|
|
104
|
+
stream: bool = False,
|
|
105
|
+
temperature: Optional[float] = None,
|
|
106
|
+
max_tokens: Optional[int] = None,
|
|
107
|
+
top_p: Optional[float] = None,
|
|
108
|
+
**kwargs: Any,
|
|
109
|
+
) -> Union[Dict[str, Any], AsyncIterator[Dict[str, Any]]]:
|
|
110
|
+
"""
|
|
111
|
+
Create a chat completion.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
model: Model name (e.g. "gpt-4o", "claude-3-5-sonnet-latest")
|
|
115
|
+
messages: List of message dicts with "role" and "content"
|
|
116
|
+
stream: If True, returns an async iterator of chunk dicts
|
|
117
|
+
temperature: Sampling temperature
|
|
118
|
+
max_tokens: Max tokens to generate
|
|
119
|
+
**kwargs: Any other OpenAI-compatible parameters
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
If stream=False: A dict matching the OpenAI ChatCompletion format.
|
|
123
|
+
If stream=True: An async iterator of chunk dicts.
|
|
124
|
+
"""
|
|
125
|
+
entries = await self._client._get_entries()
|
|
126
|
+
provider_keys: Dict[str, List[str]] = {}
|
|
127
|
+
for e in entries:
|
|
128
|
+
provider_keys.setdefault(e.provider, []).append(e.key)
|
|
129
|
+
|
|
130
|
+
primary = resolve_provider(model)
|
|
131
|
+
if not primary:
|
|
132
|
+
raise ValueError(f"No provider found for model '{model}'.")
|
|
133
|
+
|
|
134
|
+
order = [primary] + [p for p in PROVIDER_CONFIGS if p != primary]
|
|
135
|
+
payload: Dict[str, Any] = {
|
|
136
|
+
"model": model,
|
|
137
|
+
"messages": messages,
|
|
138
|
+
"stream": stream,
|
|
139
|
+
**kwargs,
|
|
140
|
+
}
|
|
141
|
+
if temperature is not None:
|
|
142
|
+
payload["temperature"] = temperature
|
|
143
|
+
if max_tokens is not None:
|
|
144
|
+
payload["max_tokens"] = max_tokens
|
|
145
|
+
if top_p is not None:
|
|
146
|
+
payload["top_p"] = top_p
|
|
147
|
+
|
|
148
|
+
errors = []
|
|
149
|
+
attempts = 0
|
|
150
|
+
|
|
151
|
+
for provider in order:
|
|
152
|
+
keys = provider_keys.get(provider, [])
|
|
153
|
+
if not keys:
|
|
154
|
+
continue
|
|
155
|
+
if attempts >= self._client.max_retries:
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
config = PROVIDER_CONFIGS.get(provider)
|
|
159
|
+
if not config:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
for key in keys:
|
|
163
|
+
if attempts >= self._client.max_retries:
|
|
164
|
+
break
|
|
165
|
+
attempts += 1
|
|
166
|
+
headers = build_headers(provider, key)
|
|
167
|
+
try:
|
|
168
|
+
async with httpx.AsyncClient(timeout=self._client.timeout) as http:
|
|
169
|
+
if stream:
|
|
170
|
+
return self._stream_response(http, config["url"], headers, payload)
|
|
171
|
+
resp = await http.post(config["url"], json=payload, headers=headers)
|
|
172
|
+
if resp.status_code == 200:
|
|
173
|
+
return resp.json()
|
|
174
|
+
errors.append(f"{provider}: HTTP {resp.status_code}")
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
errors.append(f"{provider}: {exc}")
|
|
177
|
+
|
|
178
|
+
raise RuntimeError(f"All providers failed: {'; '.join(errors)}")
|
|
179
|
+
|
|
180
|
+
async def _stream_response(
|
|
181
|
+
self,
|
|
182
|
+
http: httpx.AsyncClient,
|
|
183
|
+
url: str,
|
|
184
|
+
headers: Dict[str, str],
|
|
185
|
+
payload: Dict[str, Any],
|
|
186
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
187
|
+
import json as _json
|
|
188
|
+
async with http.stream("POST", url, json=payload, headers=headers) as resp:
|
|
189
|
+
async for line in resp.aiter_lines():
|
|
190
|
+
if line.startswith("data: "):
|
|
191
|
+
data = line[6:]
|
|
192
|
+
if data == "[DONE]":
|
|
193
|
+
break
|
|
194
|
+
try:
|
|
195
|
+
yield _json.loads(data)
|
|
196
|
+
except _json.JSONDecodeError:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Chat:
|
|
201
|
+
def __init__(self, client: "VaultEdge") -> None:
|
|
202
|
+
self.completions = Completions(client)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ─── Main Client ───────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
class VaultEdge:
|
|
208
|
+
"""
|
|
209
|
+
VaultEdge Python SDK client.
|
|
210
|
+
|
|
211
|
+
Example::
|
|
212
|
+
|
|
213
|
+
ve = VaultEdge(
|
|
214
|
+
vault=os.environ["VAULTEDGE_VAULT"],
|
|
215
|
+
password=os.environ["VAULTEDGE_PASSWORD"],
|
|
216
|
+
)
|
|
217
|
+
response = await ve.chat.completions.create(
|
|
218
|
+
model="gpt-4o",
|
|
219
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
220
|
+
)
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
vault: Optional[str] = None,
|
|
226
|
+
password: Optional[str] = None,
|
|
227
|
+
*,
|
|
228
|
+
timeout: float = 60.0,
|
|
229
|
+
max_retries: int = 3,
|
|
230
|
+
debug: bool = False,
|
|
231
|
+
) -> None:
|
|
232
|
+
self._vault_string = vault or os.environ.get("VAULTEDGE_VAULT")
|
|
233
|
+
self._password = password or os.environ.get("VAULTEDGE_PASSWORD")
|
|
234
|
+
|
|
235
|
+
if not self._vault_string:
|
|
236
|
+
raise ValueError(
|
|
237
|
+
"No vault provided. Pass vault= or set VAULTEDGE_VAULT env var."
|
|
238
|
+
)
|
|
239
|
+
if not self._password:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
"No password provided. Pass password= or set VAULTEDGE_PASSWORD env var."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self.timeout = timeout
|
|
245
|
+
self.max_retries = max_retries
|
|
246
|
+
self.debug = debug
|
|
247
|
+
self.chat = Chat(self)
|
|
248
|
+
self._cached_entries: Optional[List[VaultEntry]] = None
|
|
249
|
+
|
|
250
|
+
async def _get_entries(self) -> List[VaultEntry]:
|
|
251
|
+
if self._cached_entries is None:
|
|
252
|
+
if self.debug:
|
|
253
|
+
print("[vaultedge] Decrypting vault...")
|
|
254
|
+
self._cached_entries = decrypt_vault(self._vault_string, self._password) # type: ignore[arg-type]
|
|
255
|
+
if self.debug:
|
|
256
|
+
print(f"[vaultedge] Vault decrypted: {len(self._cached_entries)} entries")
|
|
257
|
+
return self._cached_entries
|
|
258
|
+
|
|
259
|
+
async def get_providers(self) -> List[str]:
|
|
260
|
+
"""Return list of providers available in the vault."""
|
|
261
|
+
entries = await self._get_entries()
|
|
262
|
+
return list({e.provider for e in entries})
|
|
263
|
+
|
|
264
|
+
async def has_provider(self, provider: str) -> bool:
|
|
265
|
+
"""Check if the vault has a key for the given provider."""
|
|
266
|
+
entries = await self._get_entries()
|
|
267
|
+
return any(e.provider == provider for e in entries)
|
|
268
|
+
|
|
269
|
+
def resolve_model(self, model: str) -> Optional[str]:
|
|
270
|
+
"""Resolve which provider will handle a given model name."""
|
|
271
|
+
return resolve_provider(model)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VaultEdge vault crypto — AES-256-GCM + PBKDF2.
|
|
3
|
+
|
|
4
|
+
Wire format:
|
|
5
|
+
Prefix: "VE_VAULT_v1_"
|
|
6
|
+
Payload (base64): salt[32] + nonce[12] + AES-256-GCM ciphertext+tag
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
16
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
17
|
+
from cryptography.hazmat.primitives import hashes
|
|
18
|
+
|
|
19
|
+
VAULT_PREFIX = "VE_VAULT_v1_"
|
|
20
|
+
PBKDF2_ITERATIONS = 210_000
|
|
21
|
+
SALT_BYTES = 32
|
|
22
|
+
NONCE_BYTES = 12
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class VaultEntry:
|
|
27
|
+
"""A decrypted API key entry from the vault."""
|
|
28
|
+
provider: str
|
|
29
|
+
key: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _derive_key(password: str, salt: bytes) -> bytes:
|
|
33
|
+
"""Derive a 256-bit AES key from a password using PBKDF2-HMAC-SHA256."""
|
|
34
|
+
kdf = PBKDF2HMAC(
|
|
35
|
+
algorithm=hashes.SHA256(),
|
|
36
|
+
length=32,
|
|
37
|
+
salt=salt,
|
|
38
|
+
iterations=PBKDF2_ITERATIONS,
|
|
39
|
+
)
|
|
40
|
+
return kdf.derive(password.encode("utf-8"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def encrypt_vault(entries: List[VaultEntry], password: str) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Encrypt a list of VaultEntry objects with a master password.
|
|
46
|
+
|
|
47
|
+
Returns a string starting with VE_VAULT_v1_ suitable for an env var.
|
|
48
|
+
"""
|
|
49
|
+
salt = os.urandom(SALT_BYTES)
|
|
50
|
+
nonce = os.urandom(NONCE_BYTES)
|
|
51
|
+
key = _derive_key(password, salt)
|
|
52
|
+
|
|
53
|
+
plaintext = json.dumps(
|
|
54
|
+
[{"provider": e.provider, "key": e.key} for e in entries]
|
|
55
|
+
).encode("utf-8")
|
|
56
|
+
|
|
57
|
+
aesgcm = AESGCM(key)
|
|
58
|
+
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
|
|
59
|
+
|
|
60
|
+
wire = salt + nonce + ciphertext
|
|
61
|
+
b64 = base64.b64encode(wire).decode("ascii")
|
|
62
|
+
return f"{VAULT_PREFIX}{b64}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def decrypt_vault(vault_string: str, password: str) -> List[VaultEntry]:
|
|
66
|
+
"""
|
|
67
|
+
Decrypt a VaultEdge vault string.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If the vault string is invalid or the password is wrong.
|
|
71
|
+
"""
|
|
72
|
+
if vault_string.startswith(VAULT_PREFIX):
|
|
73
|
+
b64 = vault_string[len(VAULT_PREFIX):]
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Invalid vault format. Expected string starting with '{VAULT_PREFIX}'."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
wire = base64.b64decode(b64)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
raise ValueError("Vault string is not valid base64.") from exc
|
|
83
|
+
|
|
84
|
+
if len(wire) < SALT_BYTES + NONCE_BYTES + 16:
|
|
85
|
+
raise ValueError("Vault data is too short to be valid.")
|
|
86
|
+
|
|
87
|
+
salt = wire[:SALT_BYTES]
|
|
88
|
+
nonce = wire[SALT_BYTES:SALT_BYTES + NONCE_BYTES]
|
|
89
|
+
ciphertext = wire[SALT_BYTES + NONCE_BYTES:]
|
|
90
|
+
|
|
91
|
+
key = _derive_key(password, salt)
|
|
92
|
+
aesgcm = AESGCM(key)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"Decryption failed. The password is incorrect or the vault is corrupted."
|
|
99
|
+
) from exc
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
data = json.loads(plaintext.decode("utf-8"))
|
|
103
|
+
if not isinstance(data, list):
|
|
104
|
+
raise ValueError("Vault decrypted but contained invalid JSON structure.")
|
|
105
|
+
return [VaultEntry(provider=item["provider"], key=item["key"]) for item in data]
|
|
106
|
+
except (json.JSONDecodeError, KeyError) as exc:
|
|
107
|
+
raise ValueError("Vault decrypted but contained invalid JSON.") from exc
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vaultedge
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: VaultEdge — zero-trust AI key manager SDK for Python
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://vaultedge.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/you/vaultedge
|
|
8
|
+
Keywords: ai,llm,api-key,vault,openai,security,proxy
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security :: Cryptography
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: cryptography>=42.0.0
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vaultedge
|