harpocrate 0.8.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.
- harpocrate/__init__.py +54 -0
- harpocrate/cache.py +73 -0
- harpocrate/cli.py +193 -0
- harpocrate/client.py +553 -0
- harpocrate/connect.py +213 -0
- harpocrate/crypto/__init__.py +7 -0
- harpocrate/crypto/aes_gcm.py +72 -0
- harpocrate/exceptions.py +77 -0
- harpocrate/generators/__init__.py +77 -0
- harpocrate/generators/bcrypt_password_gen.py +48 -0
- harpocrate/generators/bytes_gen.py +37 -0
- harpocrate/generators/passphrase_gen.py +68 -0
- harpocrate/generators/random_gen.py +62 -0
- harpocrate/generators/rsa_keypair_gen.py +70 -0
- harpocrate/generators/ssh_keypair_gen.py +67 -0
- harpocrate/generators/template_gen.py +80 -0
- harpocrate/generators/tls_certificate_gen.py +110 -0
- harpocrate/generators/uuid_gen.py +64 -0
- harpocrate/generators/wordlists/en.txt +550 -0
- harpocrate/generators/wordlists/fr.txt +325 -0
- harpocrate/http.py +192 -0
- harpocrate/models/__init__.py +16 -0
- harpocrate/models/secret.py +87 -0
- harpocrate/models/secret_type.py +73 -0
- harpocrate/models/wallet.py +58 -0
- harpocrate/py.typed +0 -0
- harpocrate/rotation.py +104 -0
- harpocrate/token.py +223 -0
- harpocrate-0.8.0.dist-info/METADATA +176 -0
- harpocrate-0.8.0.dist-info/RECORD +33 -0
- harpocrate-0.8.0.dist-info/WHEEL +4 -0
- harpocrate-0.8.0.dist-info/entry_points.txt +2 -0
- harpocrate-0.8.0.dist-info/licenses/LICENSE +21 -0
harpocrate/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""SDK Python Harpocrate — client zero-knowledge pour Vault.
|
|
2
|
+
|
|
3
|
+
Interface principale :
|
|
4
|
+
from harpocrate import VaultClient
|
|
5
|
+
|
|
6
|
+
client = VaultClient(
|
|
7
|
+
token="hrpv_1_...",
|
|
8
|
+
base_url="https://vault.yoops.org",
|
|
9
|
+
)
|
|
10
|
+
value = client.secrets.get("ANTHROPIC_API_KEY")
|
|
11
|
+
|
|
12
|
+
Modules :
|
|
13
|
+
harpocrate.client — VaultClient (point d'entrée principal)
|
|
14
|
+
harpocrate.token — parse_token, ParsedToken
|
|
15
|
+
harpocrate.crypto — aes_gcm_encrypt, aes_gcm_decrypt
|
|
16
|
+
harpocrate.generators — dispatch (9 générateurs)
|
|
17
|
+
harpocrate.cache — WalletKeyCache
|
|
18
|
+
harpocrate.models — SecretInfo, PopulateResult, WalletInfo, ApiKeyInfo
|
|
19
|
+
harpocrate.exceptions — toutes les exceptions SDK
|
|
20
|
+
harpocrate.cli — CLI Click (harpocrate-gen)
|
|
21
|
+
harpocrate.connect — assistant « Se connecter avec Harpocrate » (start, finish)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from harpocrate.client import VaultClient
|
|
27
|
+
from harpocrate.exceptions import (
|
|
28
|
+
ConnectError,
|
|
29
|
+
GeneratorError,
|
|
30
|
+
HarpocrateError,
|
|
31
|
+
InvalidTokenError,
|
|
32
|
+
PermissionDenied,
|
|
33
|
+
PlaceholderNotPopulated,
|
|
34
|
+
SecretNotFound,
|
|
35
|
+
TokenExpiredError,
|
|
36
|
+
VaultDecryptionError,
|
|
37
|
+
VaultHttpError,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = "0.8.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"VaultClient",
|
|
44
|
+
"HarpocrateError",
|
|
45
|
+
"ConnectError",
|
|
46
|
+
"InvalidTokenError",
|
|
47
|
+
"TokenExpiredError",
|
|
48
|
+
"PermissionDenied",
|
|
49
|
+
"VaultHttpError",
|
|
50
|
+
"VaultDecryptionError",
|
|
51
|
+
"SecretNotFound",
|
|
52
|
+
"PlaceholderNotPopulated",
|
|
53
|
+
"GeneratorError",
|
|
54
|
+
]
|
harpocrate/cache.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Cache en RAM pour la wallet_key déchiffrée — LOT_09 SDK.
|
|
2
|
+
|
|
3
|
+
La wallet_key est déchiffrée une fois depuis le token (decryption_key)
|
|
4
|
+
puis mise en cache pour éviter de la redériver à chaque appel.
|
|
5
|
+
|
|
6
|
+
Sécurité :
|
|
7
|
+
- Le cache est en mémoire uniquement, jamais persisté.
|
|
8
|
+
- TTL configurable (défaut 600s = 10 min).
|
|
9
|
+
- Si l'API key est révoquée, le serveur retournera 401 au prochain appel HTTP.
|
|
10
|
+
Le cache local peut continuer à fonctionner jusqu'au TTL expiry (acceptable MVP).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WalletKeyCache:
|
|
20
|
+
"""Cache LRU + TTL simple pour stocker la wallet_key déchiffrée.
|
|
21
|
+
|
|
22
|
+
Utilise un simple dict avec expiry timestamp (pas de dépendance cachetools
|
|
23
|
+
pour minimiser les imports du SDK).
|
|
24
|
+
|
|
25
|
+
Thread-safe via un Lock.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, ttl_seconds: int = 600, maxsize: int = 16) -> None:
|
|
29
|
+
"""Initialise le cache.
|
|
30
|
+
|
|
31
|
+
Paramètres :
|
|
32
|
+
ttl_seconds : durée de vie d'une entrée en secondes (défaut 600).
|
|
33
|
+
maxsize : nombre maximum d'entrées (LRU, les plus vieilles expirent).
|
|
34
|
+
"""
|
|
35
|
+
self._ttl = ttl_seconds
|
|
36
|
+
self._maxsize = maxsize
|
|
37
|
+
self._store: dict[str, tuple[bytes, float]] = {} # key → (value, expire_at)
|
|
38
|
+
self._lock = threading.Lock()
|
|
39
|
+
|
|
40
|
+
def get(self, cache_key: str) -> bytes | None:
|
|
41
|
+
"""Retourne la valeur si elle existe et n'est pas expirée, None sinon."""
|
|
42
|
+
with self._lock:
|
|
43
|
+
entry = self._store.get(cache_key)
|
|
44
|
+
if entry is None:
|
|
45
|
+
return None
|
|
46
|
+
value, expire_at = entry
|
|
47
|
+
if time.monotonic() > expire_at:
|
|
48
|
+
del self._store[cache_key]
|
|
49
|
+
return None
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
def set(self, cache_key: str, value: bytes) -> None:
|
|
53
|
+
"""Stocke une valeur avec TTL.
|
|
54
|
+
|
|
55
|
+
Si le cache est plein, l'entrée avec l'expiry la plus proche est évincée.
|
|
56
|
+
"""
|
|
57
|
+
with self._lock:
|
|
58
|
+
if len(self._store) >= self._maxsize and cache_key not in self._store:
|
|
59
|
+
# Éviction de l'entrée avec expire_at le plus proche (LRU approximatif)
|
|
60
|
+
oldest_key = min(self._store, key=lambda k: self._store[k][1])
|
|
61
|
+
del self._store[oldest_key]
|
|
62
|
+
expire_at = time.monotonic() + self._ttl
|
|
63
|
+
self._store[cache_key] = (value, expire_at)
|
|
64
|
+
|
|
65
|
+
def clear(self) -> None:
|
|
66
|
+
"""Vide tout le cache."""
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._store.clear()
|
|
69
|
+
|
|
70
|
+
def invalidate(self, cache_key: str) -> None:
|
|
71
|
+
"""Supprime une entrée spécifique du cache."""
|
|
72
|
+
with self._lock:
|
|
73
|
+
self._store.pop(cache_key, None)
|
harpocrate/cli.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""CLI Python harpocrate-gen — helper pour le bash CLI — LOT_09 SDK.
|
|
2
|
+
|
|
3
|
+
Ce module expose un CLI Click réutilisé par le script bash harpocrate-cli.
|
|
4
|
+
Toutes les opérations crypto (AES-GCM, RSA, etc.) passent par ce CLI.
|
|
5
|
+
|
|
6
|
+
Usage depuis bash :
|
|
7
|
+
harpocrate-gen get --token "$TOKEN" --url "$URL" --name SECRET_NAME
|
|
8
|
+
harpocrate-gen populate --token "$TOKEN" --url "$URL" --name SECRET_NAME
|
|
9
|
+
harpocrate-gen populate-all --token "$TOKEN" --url "$URL"
|
|
10
|
+
harpocrate-gen list --token "$TOKEN" --url "$URL"
|
|
11
|
+
harpocrate-gen whoami --token "$TOKEN" --url "$URL"
|
|
12
|
+
harpocrate-gen wallet-id-from-token "$TOKEN"
|
|
13
|
+
|
|
14
|
+
Sécurité :
|
|
15
|
+
- Le token n'est JAMAIS loggé ou écrit dans stdout (sauf la commande wallet-id-from-token).
|
|
16
|
+
- La sortie normale va sur stdout, les erreurs sur stderr.
|
|
17
|
+
- Les valeurs de secrets vont sur stdout RAW (sans newline supplémentaire).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
import click
|
|
27
|
+
|
|
28
|
+
from harpocrate.client import VaultClient
|
|
29
|
+
from harpocrate.exceptions import HarpocrateError
|
|
30
|
+
from harpocrate.token import parse_token
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_client(token: str | None, url: str | None) -> VaultClient:
|
|
34
|
+
"""Construit un VaultClient depuis le token et l'URL."""
|
|
35
|
+
tok = token or os.environ.get("HARPOCRATE_TOKEN") or ""
|
|
36
|
+
base = url or os.environ.get("HARPOCRATE_URL") or ""
|
|
37
|
+
|
|
38
|
+
if not tok:
|
|
39
|
+
click.echo("ERROR: token required (--token or HARPOCRATE_TOKEN)", err=True)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
if not base:
|
|
42
|
+
click.echo("ERROR: url required (--url or HARPOCRATE_URL)", err=True)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
return VaultClient(token=tok, base_url=base)
|
|
47
|
+
except HarpocrateError as exc:
|
|
48
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@click.group()
|
|
53
|
+
def main() -> None:
|
|
54
|
+
"""Harpocrate helper — opérations crypto pour le CLI bash."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@main.command("get")
|
|
58
|
+
@click.option("--token", envvar="HARPOCRATE_TOKEN", help="Token hrpv_*")
|
|
59
|
+
@click.option("--url", envvar="HARPOCRATE_URL", help="URL du serveur Vault")
|
|
60
|
+
@click.option("--name", required=True, help="Nom du secret")
|
|
61
|
+
def cmd_get(token: str | None, url: str | None, name: str) -> None:
|
|
62
|
+
"""Affiche la valeur d'un secret sur stdout (sans newline terminal)."""
|
|
63
|
+
client = _get_client(token, url)
|
|
64
|
+
try:
|
|
65
|
+
value = client.secrets.get(name)
|
|
66
|
+
click.echo(value, nl=False)
|
|
67
|
+
except HarpocrateError as exc:
|
|
68
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@main.command("populate")
|
|
73
|
+
@click.option("--token", envvar="HARPOCRATE_TOKEN", help="Token hrpv_*")
|
|
74
|
+
@click.option("--url", envvar="HARPOCRATE_URL", help="URL du serveur Vault")
|
|
75
|
+
@click.option("--name", required=True, help="Nom du secret")
|
|
76
|
+
def cmd_populate(token: str | None, url: str | None, name: str) -> None:
|
|
77
|
+
"""Peuple un placeholder avec une valeur générée localement."""
|
|
78
|
+
client = _get_client(token, url)
|
|
79
|
+
try:
|
|
80
|
+
result = client.secrets.populate(name, auto_generate=True)
|
|
81
|
+
if result.success:
|
|
82
|
+
click.echo(f"{name}: populated (v{result.generation_version})")
|
|
83
|
+
else:
|
|
84
|
+
click.echo(f"ERROR: {result.error}", err=True)
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
except HarpocrateError as exc:
|
|
87
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@main.command("populate-all")
|
|
92
|
+
@click.option("--token", envvar="HARPOCRATE_TOKEN", help="Token hrpv_*")
|
|
93
|
+
@click.option("--url", envvar="HARPOCRATE_URL", help="URL du serveur Vault")
|
|
94
|
+
def cmd_populate_all(token: str | None, url: str | None) -> None:
|
|
95
|
+
"""Peuple tous les placeholders du wallet."""
|
|
96
|
+
client = _get_client(token, url)
|
|
97
|
+
try:
|
|
98
|
+
results = client.secrets.populate_all()
|
|
99
|
+
has_error = False
|
|
100
|
+
for r in results:
|
|
101
|
+
if r.success:
|
|
102
|
+
click.echo(f"{r.name}: populated (v{r.generation_version})")
|
|
103
|
+
else:
|
|
104
|
+
click.echo(f"{r.name}: ERROR — {r.error}", err=True)
|
|
105
|
+
has_error = True
|
|
106
|
+
if not results:
|
|
107
|
+
click.echo("No placeholders to populate.")
|
|
108
|
+
if has_error:
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
except HarpocrateError as exc:
|
|
111
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@main.command("list")
|
|
116
|
+
@click.option("--token", envvar="HARPOCRATE_TOKEN", help="Token hrpv_*")
|
|
117
|
+
@click.option("--url", envvar="HARPOCRATE_URL", help="URL du serveur Vault")
|
|
118
|
+
@click.option("--json", "as_json", is_flag=True, help="Sortie JSON")
|
|
119
|
+
@click.option("--placeholder", "filter_ph", is_flag=True, help="Placeholders seulement")
|
|
120
|
+
@click.option("--valued", "filter_valued", is_flag=True, help="Secrets remplis seulement")
|
|
121
|
+
def cmd_list(
|
|
122
|
+
token: str | None,
|
|
123
|
+
url: str | None,
|
|
124
|
+
as_json: bool,
|
|
125
|
+
filter_ph: bool,
|
|
126
|
+
filter_valued: bool,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Liste les secrets du wallet."""
|
|
129
|
+
client = _get_client(token, url)
|
|
130
|
+
try:
|
|
131
|
+
resp = client.secrets.list_secrets(limit=200)
|
|
132
|
+
items = resp.secrets
|
|
133
|
+
if filter_ph:
|
|
134
|
+
items = [s for s in items if s.is_placeholder]
|
|
135
|
+
elif filter_valued:
|
|
136
|
+
items = [s for s in items if not s.is_placeholder]
|
|
137
|
+
|
|
138
|
+
if as_json:
|
|
139
|
+
click.echo(
|
|
140
|
+
json.dumps(
|
|
141
|
+
[
|
|
142
|
+
{
|
|
143
|
+
"id": str(s.id),
|
|
144
|
+
"name": s.name,
|
|
145
|
+
"is_placeholder": s.is_placeholder,
|
|
146
|
+
"tags": s.tags,
|
|
147
|
+
}
|
|
148
|
+
for s in items
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
for s in items:
|
|
154
|
+
status = "placeholder" if s.is_placeholder else "valued"
|
|
155
|
+
click.echo(f"{s.name} ({status})")
|
|
156
|
+
except HarpocrateError as exc:
|
|
157
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@main.command("whoami")
|
|
162
|
+
@click.option("--token", envvar="HARPOCRATE_TOKEN", help="Token hrpv_*")
|
|
163
|
+
@click.option("--url", envvar="HARPOCRATE_URL", help="URL du serveur Vault")
|
|
164
|
+
def cmd_whoami(token: str | None, url: str | None) -> None:
|
|
165
|
+
"""Affiche les informations de l'API key courante."""
|
|
166
|
+
tok = token or os.environ.get("HARPOCRATE_TOKEN") or ""
|
|
167
|
+
if not tok:
|
|
168
|
+
click.echo("ERROR: token required (--token or HARPOCRATE_TOKEN)", err=True)
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
try:
|
|
171
|
+
parsed = parse_token(tok)
|
|
172
|
+
click.echo(f"API key ID : {parsed.api_key_id}")
|
|
173
|
+
click.echo(f"Permissions: {parsed.permissions:#04x}")
|
|
174
|
+
click.echo(f"Expires at : {'never' if parsed.exp == 0 else parsed.exp}")
|
|
175
|
+
except HarpocrateError as exc:
|
|
176
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
177
|
+
sys.exit(1)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@main.command("wallet-id-from-token")
|
|
181
|
+
@click.argument("token_arg", metavar="TOKEN")
|
|
182
|
+
def cmd_wallet_id_from_token(token_arg: str) -> None:
|
|
183
|
+
"""Extrait l'api_key_id depuis un token hrpv_* (usage interne bash CLI)."""
|
|
184
|
+
try:
|
|
185
|
+
parsed = parse_token(token_arg)
|
|
186
|
+
click.echo(str(parsed.api_key_id), nl=False)
|
|
187
|
+
except HarpocrateError as exc:
|
|
188
|
+
click.echo(f"ERROR: {exc}", err=True)
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|