@agxnte/reidx 2.0.2
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.
- package/README.md +724 -0
- package/bin/reidx.js +33 -0
- package/package.json +40 -0
- package/pyproject.toml +45 -0
- package/scripts/find-python.js +24 -0
- package/scripts/postinstall.js +35 -0
- package/src/reidx/__init__.py +3 -0
- package/src/reidx/__main__.py +9 -0
- package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/__init__.py +3 -0
- package/src/reidx/app/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/app/commands.py +221 -0
- package/src/reidx/automation/__init__.py +3 -0
- package/src/reidx/automation/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/automation/__pycache__/exec.cpython-312.pyc +0 -0
- package/src/reidx/automation/exec.py +35 -0
- package/src/reidx/config/__init__.py +10 -0
- package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
- package/src/reidx/config/loader.py +104 -0
- package/src/reidx/config/models.py +49 -0
- package/src/reidx/config/settings.py +127 -0
- package/src/reidx/deepreid/__init__.py +8 -0
- package/src/reidx/deepreid/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/deepreid/__pycache__/pipeline.cpython-312.pyc +0 -0
- package/src/reidx/deepreid/pipeline.py +221 -0
- package/src/reidx/diagnostics/__init__.py +0 -0
- package/src/reidx/diagnostics/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/diagnostics/__pycache__/logger.cpython-312.pyc +0 -0
- package/src/reidx/diagnostics/logger.py +56 -0
- package/src/reidx/goals/__init__.py +21 -0
- package/src/reidx/goals/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/goals/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/goals/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/goals/models.py +101 -0
- package/src/reidx/goals/store.py +233 -0
- package/src/reidx/integrations/__init__.py +3 -0
- package/src/reidx/integrations/mcp.py +70 -0
- package/src/reidx/nyx/__init__.py +3 -0
- package/src/reidx/nyx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/nyx/__pycache__/persona.cpython-312.pyc +0 -0
- package/src/reidx/nyx/persona.py +35 -0
- package/src/reidx/policy/__init__.py +15 -0
- package/src/reidx/policy/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/policy/__pycache__/engine.cpython-312.pyc +0 -0
- package/src/reidx/policy/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/policy/engine.py +101 -0
- package/src/reidx/policy/models.py +35 -0
- package/src/reidx/provider/__init__.py +16 -0
- package/src/reidx/provider/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/anthropic.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/registry.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/stub.cpython-312.pyc +0 -0
- package/src/reidx/provider/_http.py +29 -0
- package/src/reidx/provider/anthropic.py +176 -0
- package/src/reidx/provider/base.py +50 -0
- package/src/reidx/provider/ollama.py +109 -0
- package/src/reidx/provider/openai.py +144 -0
- package/src/reidx/provider/registry.py +88 -0
- package/src/reidx/provider/store.py +141 -0
- package/src/reidx/provider/stub.py +60 -0
- package/src/reidx/provider_manager/__init__.py +40 -0
- package/src/reidx/provider_manager/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/catalog.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/database.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/keychain.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/palette.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/catalog.py +330 -0
- package/src/reidx/provider_manager/database.py +234 -0
- package/src/reidx/provider_manager/keychain.py +102 -0
- package/src/reidx/provider_manager/palette.py +831 -0
- package/src/reidx/runtime/__init__.py +5 -0
- package/src/reidx/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/agent.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/reasoning.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/state.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/subagent.cpython-312.pyc +0 -0
- package/src/reidx/runtime/agent.py +198 -0
- package/src/reidx/runtime/orchestrator.py +244 -0
- package/src/reidx/runtime/reasoning.py +77 -0
- package/src/reidx/runtime/state.py +32 -0
- package/src/reidx/runtime/subagent.py +174 -0
- package/src/reidx/session/__init__.py +4 -0
- package/src/reidx/session/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/session/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/session/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/session/models.py +43 -0
- package/src/reidx/session/store.py +101 -0
- package/src/reidx/tasks/__init__.py +4 -0
- package/src/reidx/tasks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/tasks/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/tasks/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/tasks/models.py +44 -0
- package/src/reidx/tasks/store.py +86 -0
- package/src/reidx/tools/__init__.py +44 -0
- package/src/reidx/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/registry.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/shell_tool.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/spawn_agent.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
- package/src/reidx/tools/base.py +84 -0
- package/src/reidx/tools/file_tools.py +222 -0
- package/src/reidx/tools/registry.py +45 -0
- package/src/reidx/tools/shell_tool.py +75 -0
- package/src/reidx/tools/spawn_agent.py +185 -0
- package/src/reidx/tools/web_tools.py +216 -0
- package/src/reidx/ui/__init__.py +41 -0
- package/src/reidx/ui/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/app.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/render.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/repl.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/theme.cpython-312.pyc +0 -0
- package/src/reidx/ui/app.py +1343 -0
- package/src/reidx/ui/commands.py +663 -0
- package/src/reidx/ui/render.py +517 -0
- package/src/reidx/ui/repl.py +14 -0
- package/src/reidx/ui/theme.py +126 -0
- package/src/reidx/workflows/__init__.py +4 -0
- package/src/reidx/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/workflows/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/workflows/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/workflows/models.py +19 -0
- package/src/reidx/workflows/store.py +56 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from reidx.diagnostics.logger import get_logger
|
|
10
|
+
from reidx.provider_manager import keychain
|
|
11
|
+
|
|
12
|
+
log = get_logger("reidx.provider_manager.database")
|
|
13
|
+
|
|
14
|
+
SCHEMA_VERSION = 1
|
|
15
|
+
DB_FILENAME = "providers.db"
|
|
16
|
+
LEGACY_FILENAME = "providers.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class StoredKey:
|
|
21
|
+
id: str
|
|
22
|
+
label: str
|
|
23
|
+
encrypted_key: str
|
|
24
|
+
|
|
25
|
+
def decrypt(self) -> str:
|
|
26
|
+
return keychain.decrypt(self.encrypted_key)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class StoredProvider:
|
|
31
|
+
name: str
|
|
32
|
+
kind: str
|
|
33
|
+
base_url: str = ""
|
|
34
|
+
default_model: str = ""
|
|
35
|
+
auth_method: str = "bearer"
|
|
36
|
+
extra_headers: dict[str, str] = field(default_factory=dict)
|
|
37
|
+
catalog_id: str | None = None
|
|
38
|
+
keys: list[StoredKey] = field(default_factory=list)
|
|
39
|
+
active_key_id: str | None = None
|
|
40
|
+
|
|
41
|
+
def active_key(self) -> StoredKey | None:
|
|
42
|
+
if not self.active_key_id:
|
|
43
|
+
return self.keys[0] if self.keys else None
|
|
44
|
+
for k in self.keys:
|
|
45
|
+
if k.id == self.active_key_id:
|
|
46
|
+
return k
|
|
47
|
+
return self.keys[0] if self.keys else None
|
|
48
|
+
|
|
49
|
+
def decrypted_api_key(self) -> str:
|
|
50
|
+
k = self.active_key()
|
|
51
|
+
return k.decrypt() if k else ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ProviderDatabase:
|
|
55
|
+
def __init__(self, storage_root: Path) -> None:
|
|
56
|
+
self.path = Path(storage_root) / DB_FILENAME
|
|
57
|
+
self.legacy_path = Path(storage_root) / LEGACY_FILENAME
|
|
58
|
+
self._migrate_legacy_if_needed()
|
|
59
|
+
|
|
60
|
+
def _migrate_legacy_if_needed(self) -> None:
|
|
61
|
+
if self.path.exists() or not self.legacy_path.exists():
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
data = json.loads(self.legacy_path.read_text(encoding="utf-8"))
|
|
65
|
+
except (OSError, ValueError):
|
|
66
|
+
log.warning("failed to read legacy providers.json; skipping migration")
|
|
67
|
+
return
|
|
68
|
+
records = data.get("providers", [])
|
|
69
|
+
if not records:
|
|
70
|
+
return
|
|
71
|
+
log.info("migrating %d provider(s) from legacy providers.json", len(records))
|
|
72
|
+
providers: list[dict] = []
|
|
73
|
+
for entry in records:
|
|
74
|
+
name = entry.get("name", "")
|
|
75
|
+
if not name:
|
|
76
|
+
continue
|
|
77
|
+
api_key = entry.get("api_key", "")
|
|
78
|
+
keys: list[dict] = []
|
|
79
|
+
active_key_id = None
|
|
80
|
+
if api_key:
|
|
81
|
+
kid = uuid.uuid4().hex[:12]
|
|
82
|
+
keys.append({
|
|
83
|
+
"id": kid,
|
|
84
|
+
"label": "Imported",
|
|
85
|
+
"encrypted_key": keychain.encrypt(api_key),
|
|
86
|
+
})
|
|
87
|
+
active_key_id = kid
|
|
88
|
+
providers.append({
|
|
89
|
+
"name": name,
|
|
90
|
+
"kind": entry.get("kind", "openai-compatible"),
|
|
91
|
+
"base_url": entry.get("base_url", ""),
|
|
92
|
+
"default_model": entry.get("default_model", ""),
|
|
93
|
+
"auth_method": "bearer",
|
|
94
|
+
"extra_headers": {},
|
|
95
|
+
"catalog_id": None,
|
|
96
|
+
"keys": keys,
|
|
97
|
+
"active_key_id": active_key_id,
|
|
98
|
+
})
|
|
99
|
+
self._write_raw({"version": SCHEMA_VERSION, "providers": providers})
|
|
100
|
+
try:
|
|
101
|
+
os.rename(self.legacy_path, self.legacy_path.with_suffix(".json.bak"))
|
|
102
|
+
except OSError:
|
|
103
|
+
pass
|
|
104
|
+
log.info("migration complete; legacy file backed up")
|
|
105
|
+
|
|
106
|
+
def _read_raw(self) -> dict:
|
|
107
|
+
if not self.path.exists():
|
|
108
|
+
return {"version": SCHEMA_VERSION, "providers": []}
|
|
109
|
+
try:
|
|
110
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
111
|
+
except (OSError, ValueError):
|
|
112
|
+
log.exception("failed to read providers.db; treating as empty")
|
|
113
|
+
return {"version": SCHEMA_VERSION, "providers": []}
|
|
114
|
+
if data.get("version", 0) > SCHEMA_VERSION:
|
|
115
|
+
log.warning("providers.db version %s > supported %s; some features may not work",
|
|
116
|
+
data.get("version"), SCHEMA_VERSION)
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
def _write_raw(self, data: dict) -> None:
|
|
120
|
+
keychain.secure_write(self.path, json.dumps(data, indent=2))
|
|
121
|
+
|
|
122
|
+
def _to_stored(self, entry: dict) -> StoredProvider:
|
|
123
|
+
keys = [StoredKey(
|
|
124
|
+
id=k.get("id", ""),
|
|
125
|
+
label=k.get("label", ""),
|
|
126
|
+
encrypted_key=k.get("encrypted_key", ""),
|
|
127
|
+
) for k in entry.get("keys", [])]
|
|
128
|
+
return StoredProvider(
|
|
129
|
+
name=entry.get("name", ""),
|
|
130
|
+
kind=entry.get("kind", "openai-compatible"),
|
|
131
|
+
base_url=entry.get("base_url", ""),
|
|
132
|
+
default_model=entry.get("default_model", ""),
|
|
133
|
+
auth_method=entry.get("auth_method", "bearer"),
|
|
134
|
+
extra_headers=entry.get("extra_headers", {}),
|
|
135
|
+
catalog_id=entry.get("catalog_id"),
|
|
136
|
+
keys=keys,
|
|
137
|
+
active_key_id=entry.get("active_key_id"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _to_dict(self, p: StoredProvider) -> dict:
|
|
141
|
+
return {
|
|
142
|
+
"name": p.name,
|
|
143
|
+
"kind": p.kind,
|
|
144
|
+
"base_url": p.base_url,
|
|
145
|
+
"default_model": p.default_model,
|
|
146
|
+
"auth_method": p.auth_method,
|
|
147
|
+
"extra_headers": p.extra_headers,
|
|
148
|
+
"catalog_id": p.catalog_id,
|
|
149
|
+
"keys": [asdict(k) for k in p.keys],
|
|
150
|
+
"active_key_id": p.active_key_id,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def list_providers(self) -> list[StoredProvider]:
|
|
154
|
+
data = self._read_raw()
|
|
155
|
+
return [self._to_stored(e) for e in data.get("providers", [])]
|
|
156
|
+
|
|
157
|
+
def get_provider(self, name: str) -> StoredProvider | None:
|
|
158
|
+
for p in self.list_providers():
|
|
159
|
+
if p.name == name:
|
|
160
|
+
return p
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def save_provider(self, provider: StoredProvider) -> None:
|
|
164
|
+
data = self._read_raw()
|
|
165
|
+
providers = data.get("providers", [])
|
|
166
|
+
providers = [e for e in providers if e.get("name") != provider.name]
|
|
167
|
+
providers.append(self._to_dict(provider))
|
|
168
|
+
data["providers"] = providers
|
|
169
|
+
self._write_raw(data)
|
|
170
|
+
|
|
171
|
+
def remove_provider(self, name: str) -> bool:
|
|
172
|
+
data = self._read_raw()
|
|
173
|
+
providers = data.get("providers", [])
|
|
174
|
+
remaining = [e for e in providers if e.get("name") != name]
|
|
175
|
+
if len(remaining) == len(providers):
|
|
176
|
+
return False
|
|
177
|
+
data["providers"] = remaining
|
|
178
|
+
self._write_raw(data)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
def add_key(self, provider_name: str, label: str, api_key: str) -> StoredKey | None:
|
|
182
|
+
p = self.get_provider(provider_name)
|
|
183
|
+
if p is None:
|
|
184
|
+
return None
|
|
185
|
+
key = StoredKey(
|
|
186
|
+
id=uuid.uuid4().hex[:12],
|
|
187
|
+
label=label,
|
|
188
|
+
encrypted_key=keychain.encrypt(api_key),
|
|
189
|
+
)
|
|
190
|
+
p.keys.append(key)
|
|
191
|
+
if p.active_key_id is None:
|
|
192
|
+
p.active_key_id = key.id
|
|
193
|
+
self.save_provider(p)
|
|
194
|
+
return key
|
|
195
|
+
|
|
196
|
+
def remove_key(self, provider_name: str, key_id: str) -> bool:
|
|
197
|
+
p = self.get_provider(provider_name)
|
|
198
|
+
if p is None:
|
|
199
|
+
return False
|
|
200
|
+
before = len(p.keys)
|
|
201
|
+
p.keys = [k for k in p.keys if k.id != key_id]
|
|
202
|
+
if len(p.keys) == before:
|
|
203
|
+
return False
|
|
204
|
+
if p.active_key_id == key_id:
|
|
205
|
+
p.active_key_id = p.keys[0].id if p.keys else None
|
|
206
|
+
self.save_provider(p)
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
def rename_key(self, provider_name: str, key_id: str, new_label: str) -> bool:
|
|
210
|
+
p = self.get_provider(provider_name)
|
|
211
|
+
if p is None:
|
|
212
|
+
return False
|
|
213
|
+
for k in p.keys:
|
|
214
|
+
if k.id == key_id:
|
|
215
|
+
k.label = new_label
|
|
216
|
+
self.save_provider(p)
|
|
217
|
+
return True
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
def set_active_key(self, provider_name: str, key_id: str) -> bool:
|
|
221
|
+
p = self.get_provider(provider_name)
|
|
222
|
+
if p is None:
|
|
223
|
+
return False
|
|
224
|
+
if not any(k.id == key_id for k in p.keys):
|
|
225
|
+
return False
|
|
226
|
+
p.active_key_id = key_id
|
|
227
|
+
self.save_provider(p)
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
def get_decrypted_key(self, provider_name: str) -> str:
|
|
231
|
+
p = self.get_provider(provider_name)
|
|
232
|
+
if p is None:
|
|
233
|
+
return ""
|
|
234
|
+
return p.decrypted_api_key()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from reidx.diagnostics.logger import get_logger
|
|
9
|
+
|
|
10
|
+
log = get_logger("reidx.provider_manager.keychain")
|
|
11
|
+
|
|
12
|
+
_IS_WINDOWS = sys.platform == "win32"
|
|
13
|
+
|
|
14
|
+
if _IS_WINDOWS:
|
|
15
|
+
import ctypes
|
|
16
|
+
import ctypes.wintypes
|
|
17
|
+
|
|
18
|
+
class _DATA_BLOB(ctypes.Structure):
|
|
19
|
+
_fields_ = [
|
|
20
|
+
("cbData", ctypes.wintypes.DWORD),
|
|
21
|
+
("pbData", ctypes.POINTER(ctypes.c_char)),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def _dpapi_encrypt(plaintext: bytes) -> bytes:
|
|
25
|
+
blob_in = _DATA_BLOB()
|
|
26
|
+
blob_in.pbData = ctypes.cast(
|
|
27
|
+
ctypes.create_string_buffer(plaintext, len(plaintext)),
|
|
28
|
+
ctypes.POINTER(ctypes.c_char),
|
|
29
|
+
)
|
|
30
|
+
blob_in.cbData = len(plaintext)
|
|
31
|
+
blob_out = _DATA_BLOB()
|
|
32
|
+
ok = ctypes.windll.crypt32.CryptProtectData(
|
|
33
|
+
ctypes.byref(blob_in),
|
|
34
|
+
None,
|
|
35
|
+
None,
|
|
36
|
+
None,
|
|
37
|
+
None,
|
|
38
|
+
0,
|
|
39
|
+
ctypes.byref(blob_out),
|
|
40
|
+
)
|
|
41
|
+
if not ok:
|
|
42
|
+
raise OSError("DPAPI CryptProtectData failed")
|
|
43
|
+
try:
|
|
44
|
+
return ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
|
45
|
+
finally:
|
|
46
|
+
ctypes.windll.kernel32.LocalFree(blob_out.pbData)
|
|
47
|
+
|
|
48
|
+
def _dpapi_decrypt(ciphertext: bytes) -> bytes:
|
|
49
|
+
blob_in = _DATA_BLOB()
|
|
50
|
+
blob_in.pbData = ctypes.cast(
|
|
51
|
+
ctypes.create_string_buffer(ciphertext, len(ciphertext)),
|
|
52
|
+
ctypes.POINTER(ctypes.c_char),
|
|
53
|
+
)
|
|
54
|
+
blob_in.cbData = len(ciphertext)
|
|
55
|
+
blob_out = _DATA_BLOB()
|
|
56
|
+
ok = ctypes.windll.crypt32.CryptUnprotectData(
|
|
57
|
+
ctypes.byref(blob_in),
|
|
58
|
+
None,
|
|
59
|
+
None,
|
|
60
|
+
None,
|
|
61
|
+
None,
|
|
62
|
+
0,
|
|
63
|
+
ctypes.byref(blob_out),
|
|
64
|
+
)
|
|
65
|
+
if not ok:
|
|
66
|
+
raise OSError("DPAPI CryptUnprotectData failed")
|
|
67
|
+
try:
|
|
68
|
+
return ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
|
69
|
+
finally:
|
|
70
|
+
ctypes.windll.kernel32.LocalFree(blob_out.pbData)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def encrypt(plaintext: str) -> str:
|
|
74
|
+
if not plaintext:
|
|
75
|
+
return ""
|
|
76
|
+
raw = plaintext.encode("utf-8")
|
|
77
|
+
if _IS_WINDOWS:
|
|
78
|
+
encrypted = _dpapi_encrypt(raw)
|
|
79
|
+
else:
|
|
80
|
+
log.warning("DPAPI not available on this platform; storing key without OS encryption")
|
|
81
|
+
encrypted = raw
|
|
82
|
+
return base64.b64encode(encrypted).decode("ascii")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def decrypt(ciphertext: str) -> str:
|
|
86
|
+
if not ciphertext:
|
|
87
|
+
return ""
|
|
88
|
+
raw = base64.b64decode(ciphertext)
|
|
89
|
+
if _IS_WINDOWS:
|
|
90
|
+
decrypted = _dpapi_decrypt(raw)
|
|
91
|
+
else:
|
|
92
|
+
decrypted = raw
|
|
93
|
+
return decrypted.decode("utf-8")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def secure_write(path: Path, data: str) -> None:
|
|
97
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
path.write_text(data, encoding="utf-8")
|
|
99
|
+
try:
|
|
100
|
+
os.chmod(path, 0o600)
|
|
101
|
+
except OSError:
|
|
102
|
+
pass
|