poixe-encrypt 0.1.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.
@@ -0,0 +1,28 @@
1
+ # Go binaries
2
+ cli/cli
3
+ cli/poixe
4
+ *.exe
5
+
6
+ # Go build cache
7
+ *.test
8
+ *.out
9
+
10
+ # Python
11
+ __pycache__/
12
+ *.py[cod]
13
+ *.egg-info/
14
+ dist/
15
+ build/
16
+ .venv/
17
+ venv/
18
+
19
+ # macOS
20
+ .DS_Store
21
+
22
+ # Editor
23
+ .idea/
24
+ .vscode/
25
+ *.swp
26
+
27
+ # local
28
+ tmp
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.5
2
+ Name: poixe-encrypt
3
+ Version: 0.1.0
4
+ Summary: Client-side API key encryption for Poixe channels
5
+ Project-URL: Homepage, https://github.com/poixeai/poixe-security
6
+ Project-URL: Repository, https://github.com/poixeai/poixe-security
7
+ License: MIT
8
+ Keywords: api-key,encryption,poixe
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.8
19
+ Requires-Dist: cryptography>=41.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # poixe-encrypt
23
+
24
+ Python SDK for [Poixe](https://poixe.com) API key encryption.
25
+
26
+ Encrypt your API keys **locally** before submitting them to Poixe. The plaintext key never leaves your machine — only the `enc:v2:...` ciphertext is transmitted.
27
+
28
+ ## Installation
29
+
30
+ ```sh
31
+ pip install poixe-encrypt
32
+ ```
33
+
34
+ Requires Python 3.8+ and the `cryptography` package.
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from poixe_encrypt import ChannelType, encrypt
40
+
41
+ # OpenAI
42
+ cipher = encrypt(ChannelType.OPENAI_CHATGPT, "sk-proj-...")
43
+
44
+ # Anthropic
45
+ cipher = encrypt(ChannelType.ANTHROPIC_CLAUDE, "sk-ant-...")
46
+
47
+ # Azure (requires endpoint)
48
+ cipher = encrypt(ChannelType.AZURE_CHATGPT, "my-key",
49
+ endpoint="https://my-resource.openai.azure.com")
50
+
51
+ # AWS Bedrock (requires region)
52
+ cipher = encrypt(ChannelType.AWS_BEDROCK_CLAUDE, "AKID...",
53
+ region="us-east-1")
54
+
55
+ # Google Vertex Gemini (Service Account JSON string)
56
+ import json
57
+ sa = json.loads(open("sa.json").read())
58
+ cipher = encrypt(ChannelType.GOOGLE_VERTEX_GEMINI, json.dumps(sa))
59
+ ```
60
+
61
+ Output is a single `enc:v2:...` ciphertext string.
62
+
63
+ ## Public Key
64
+
65
+ The public key is fetched from `https://poixe.com/security.json` and cached for the process lifetime. It can also be pinned by fingerprint:
66
+
67
+ ```python
68
+ cipher = encrypt(ChannelType.OPENAI_CHATGPT, "sk-proj-...",
69
+ fingerprint="19bb2f01...")
70
+ ```
71
+
72
+ ## Supported Channel Types
73
+
74
+ All 32+ channel types are available as constants on `ChannelType`. See the [full list](https://github.com/poixeai/poixe-security/blob/main/docs/channel-types.md).
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,57 @@
1
+ # poixe-encrypt
2
+
3
+ Python SDK for [Poixe](https://poixe.com) API key encryption.
4
+
5
+ Encrypt your API keys **locally** before submitting them to Poixe. The plaintext key never leaves your machine — only the `enc:v2:...` ciphertext is transmitted.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pip install poixe-encrypt
11
+ ```
12
+
13
+ Requires Python 3.8+ and the `cryptography` package.
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from poixe_encrypt import ChannelType, encrypt
19
+
20
+ # OpenAI
21
+ cipher = encrypt(ChannelType.OPENAI_CHATGPT, "sk-proj-...")
22
+
23
+ # Anthropic
24
+ cipher = encrypt(ChannelType.ANTHROPIC_CLAUDE, "sk-ant-...")
25
+
26
+ # Azure (requires endpoint)
27
+ cipher = encrypt(ChannelType.AZURE_CHATGPT, "my-key",
28
+ endpoint="https://my-resource.openai.azure.com")
29
+
30
+ # AWS Bedrock (requires region)
31
+ cipher = encrypt(ChannelType.AWS_BEDROCK_CLAUDE, "AKID...",
32
+ region="us-east-1")
33
+
34
+ # Google Vertex Gemini (Service Account JSON string)
35
+ import json
36
+ sa = json.loads(open("sa.json").read())
37
+ cipher = encrypt(ChannelType.GOOGLE_VERTEX_GEMINI, json.dumps(sa))
38
+ ```
39
+
40
+ Output is a single `enc:v2:...` ciphertext string.
41
+
42
+ ## Public Key
43
+
44
+ The public key is fetched from `https://poixe.com/security.json` and cached for the process lifetime. It can also be pinned by fingerprint:
45
+
46
+ ```python
47
+ cipher = encrypt(ChannelType.OPENAI_CHATGPT, "sk-proj-...",
48
+ fingerprint="19bb2f01...")
49
+ ```
50
+
51
+ ## Supported Channel Types
52
+
53
+ All 32+ channel types are available as constants on `ChannelType`. See the [full list](https://github.com/poixeai/poixe-security/blob/main/docs/channel-types.md).
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,45 @@
1
+ """Example usage of the poixe-encrypt Python SDK."""
2
+
3
+ from poixe_encrypt import ChannelType, encrypt
4
+
5
+ # Basic: encrypt an OpenAI key
6
+ cipher = encrypt(ChannelType.OPENAI_CHATGPT, "sk-proj-example")
7
+ print("OpenAI:", cipher)
8
+
9
+ # Azure ChatGPT: requires endpoint
10
+ cipher = encrypt(
11
+ ChannelType.AZURE_CHATGPT,
12
+ "my-azure-key",
13
+ endpoint="https://my-resource.openai.azure.com",
14
+ )
15
+ print("Azure ChatGPT:", cipher)
16
+
17
+ # AWS Bedrock: requires region
18
+ cipher = encrypt(
19
+ ChannelType.AWS_BEDROCK_CLAUDE,
20
+ "AKIDEXAMPLE",
21
+ region="us-east-1",
22
+ )
23
+ print("AWS Bedrock Claude:", cipher)
24
+
25
+ # AWS Platform Claude: key is "workspace_id api_key" space-separated
26
+ cipher = encrypt(
27
+ ChannelType.AWS_PLATFORM_CLAUDE,
28
+ "ws-123 AKIDEXAMPLE",
29
+ region="eu-central-1",
30
+ )
31
+ print("AWS Platform Claude:", cipher)
32
+
33
+ # Anthropic
34
+ cipher = encrypt(ChannelType.ANTHROPIC_CLAUDE, "sk-ant-example")
35
+ print("Anthropic Claude:", cipher)
36
+
37
+ # Google Vertex Gemini: key is SA JSON (pass the JSON string)
38
+ import json
39
+ sa_json = json.dumps({
40
+ "project_id": "my-project",
41
+ "client_email": "sa@my-project.iam.gserviceaccount.com",
42
+ "private_key": "-----BEGIN RSA PRIVATE KEY-----\nEXAMPLE\n-----END RSA PRIVATE KEY-----\n",
43
+ })
44
+ cipher = encrypt(ChannelType.GOOGLE_VERTEX_GEMINI, sa_json)
45
+ print("Google Vertex Gemini:", cipher)
@@ -0,0 +1,12 @@
1
+ """poixe-encrypt: Client-side API key encryption for Poixe channels."""
2
+
3
+ from .encrypt import DEFAULT_PUBKEY_URL, encrypt, set_pubkey_pem, set_pubkey_url
4
+ from .types import ChannelType
5
+
6
+ __all__ = [
7
+ "encrypt",
8
+ "set_pubkey_url",
9
+ "set_pubkey_pem",
10
+ "DEFAULT_PUBKEY_URL",
11
+ "ChannelType",
12
+ ]
@@ -0,0 +1,226 @@
1
+ """Core encryption implementation for Poixe API key envelopes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import threading
10
+ import urllib.error
11
+ import urllib.request
12
+ from typing import Optional
13
+ from urllib.parse import urlparse
14
+
15
+ from cryptography.hazmat.primitives import hashes, serialization
16
+ from cryptography.hazmat.primitives.asymmetric import padding
17
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
18
+
19
+ DEFAULT_PUBKEY_URL = "https://poixe.com/security.json"
20
+
21
+ _DEFAULT_AWS_REGION = "eu-central-1"
22
+ _VALID_AWS_REGIONS = [
23
+ "eu-central-1", "eu-west-1", "eu-west-2",
24
+ "us-east-1", "us-east-2", "us-west-2",
25
+ ]
26
+
27
+ _CHANNEL_TYPES: dict[str, dict] = {
28
+ "openai_chatgpt": {"type_int": 2},
29
+ "azure_chatgpt": {"type_int": 3, "needs_endpoint": True},
30
+ "deepseek_deepseek": {"type_int": 4},
31
+ "anthropic_claude": {"type_int": 5},
32
+ "google_aistudio_gemini":{"type_int": 6},
33
+ "xai_grok": {"type_int": 7},
34
+ "volcengine_deepseek": {"type_int": 8},
35
+ "volcengine_doubao": {"type_int": 9},
36
+ "aliyun_deepseek": {"type_int": 10},
37
+ "aliyun_qwen": {"type_int": 11},
38
+ "siliconflow_deepseek": {"type_int": 12},
39
+ "siliconflow_qwen": {"type_int": 13},
40
+ "chutes_deepseek": {"type_int": 14},
41
+ "chutes_qwen": {"type_int": 15},
42
+ "moonshot_kimi": {"type_int": 16},
43
+ "aliyun_kimi": {"type_int": 17},
44
+ "siliconflow_kimi": {"type_int": 18},
45
+ "chutes_kimi": {"type_int": 19},
46
+ "chutes_chatgpt": {"type_int": 20},
47
+ "cerebras_chatgpt": {"type_int": 21},
48
+ "groq_chatgpt": {"type_int": 22},
49
+ "fireworks_chatgpt": {"type_int": 23},
50
+ "novita_chatgpt": {"type_int": 24},
51
+ "together_chatgpt": {"type_int": 25},
52
+ "nebius_chatgpt": {"type_int": 26},
53
+ "aws_bedrock_claude": {"type_int": 29, "needs_region": True},
54
+ "aws_platform_claude": {"type_int": 30, "needs_region": True},
55
+ "azure_claude": {"type_int": 31, "needs_endpoint": True},
56
+ "google_vertex_gemini": {"type_int": 32},
57
+ "openrouter_chatgpt": {"type_int": 33},
58
+ "openrouter_claude": {"type_int": 34},
59
+ "openrouter_gemini": {"type_int": 35},
60
+ }
61
+
62
+ # Package-level state (protected by _lock)
63
+ _lock = threading.Lock()
64
+ _pubkey_url: str = DEFAULT_PUBKEY_URL
65
+ _override_pem: Optional[str] = None
66
+ _override_kid: Optional[str] = None
67
+ _cached_key: Optional[dict] = None # {"kid": str, "key": RSAPublicKey, "fingerprint": str}
68
+
69
+
70
+ def set_pubkey_url(url: str) -> None:
71
+ """Override the URL used to fetch the public key."""
72
+ global _pubkey_url, _cached_key
73
+ with _lock:
74
+ _pubkey_url = url
75
+ _cached_key = None
76
+
77
+
78
+ def set_pubkey_pem(pem_str: str, kid: str) -> None:
79
+ """Use a local PEM key instead of fetching from the network."""
80
+ global _override_pem, _override_kid, _cached_key
81
+ with _lock:
82
+ _override_pem = pem_str
83
+ _override_kid = kid
84
+ _cached_key = None
85
+
86
+
87
+ def encrypt(
88
+ channel_type: str,
89
+ api_key: str,
90
+ *,
91
+ endpoint: Optional[str] = None,
92
+ region: Optional[str] = None,
93
+ fingerprint: Optional[str] = None,
94
+ ) -> str:
95
+ """Encrypt api_key for channel_type. Returns an enc:v2:... ciphertext string.
96
+
97
+ Args:
98
+ channel_type: One of the ChannelType constants (e.g. ChannelType.OPENAI_CHATGPT).
99
+ api_key: The API key to encrypt. Must not be empty.
100
+ endpoint: Azure endpoint URL (required for azure_chatgpt / azure_claude).
101
+ region: AWS region (for aws_bedrock_claude / aws_platform_claude). Defaults to eu-central-1.
102
+ fingerprint: Expected fingerprint prefix (hex, >= 16 chars). Raises on mismatch.
103
+
104
+ Returns:
105
+ enc:v2:<kid>:<wrapped_key>:<nonce>:<ciphertext> (base64url, no padding)
106
+ """
107
+ if channel_type not in _CHANNEL_TYPES:
108
+ raise ValueError(f"Unknown channel type: {channel_type!r}")
109
+ if not api_key:
110
+ raise ValueError("api_key must not be empty")
111
+
112
+ spec = _CHANNEL_TYPES[channel_type]
113
+ params: dict = {}
114
+
115
+ if spec.get("needs_endpoint"):
116
+ if not endpoint:
117
+ raise ValueError(f"channel type {channel_type!r} requires endpoint=")
118
+ params["endpoint"] = _normalize_azure_endpoint(endpoint)
119
+
120
+ if spec.get("needs_region"):
121
+ r = region or _DEFAULT_AWS_REGION
122
+ if r not in _VALID_AWS_REGIONS:
123
+ raise ValueError(f"Invalid AWS region {r!r}; valid: {', '.join(_VALID_AWS_REGIONS)}")
124
+ params["region"] = r
125
+
126
+ loaded = _get_key()
127
+
128
+ if fingerprint:
129
+ _check_fingerprint(loaded, fingerprint)
130
+
131
+ return _seal(loaded["key"], loaded["kid"], spec["type_int"], api_key, params)
132
+
133
+
134
+ # ---- Internal ----
135
+
136
+ def _seal(pub_key, kid: str, type_int: int, api_key: str, params: dict) -> str:
137
+ payload = {"v": 2, "t": type_int, "k": api_key}
138
+ if params:
139
+ payload["p"] = params
140
+ plain = json.dumps(payload, separators=(",", ":")).encode()
141
+
142
+ aes_key = os.urandom(32)
143
+ nonce = os.urandom(12)
144
+ aad = f"px-egress-v2|{kid}".encode()
145
+
146
+ aesgcm = AESGCM(aes_key)
147
+ ct = aesgcm.encrypt(nonce, plain, aad)
148
+
149
+ wrapped = pub_key.encrypt(
150
+ aes_key,
151
+ padding.OAEP(
152
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
153
+ algorithm=hashes.SHA256(),
154
+ label=None,
155
+ ),
156
+ )
157
+
158
+ b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
159
+ return f"enc:v2:{kid}:{b64(wrapped)}:{b64(nonce)}:{b64(ct)}"
160
+
161
+
162
+ def _get_key() -> dict:
163
+ global _cached_key
164
+ with _lock:
165
+ if _cached_key is not None:
166
+ return _cached_key
167
+ if _override_pem:
168
+ k = _parse_pem(_override_kid or "k1", _override_pem)
169
+ else:
170
+ k = _fetch_key(_pubkey_url)
171
+ _cached_key = k
172
+ return k
173
+
174
+
175
+ def _fetch_key(url: str) -> dict:
176
+ if not url.startswith("https://"):
177
+ raise ValueError("pubkey URL must be https")
178
+ req = urllib.request.Request(url, headers={"User-Agent": "pxencrypt-python/0.1"})
179
+ try:
180
+ with urllib.request.urlopen(req, timeout=15) as resp:
181
+ doc = json.loads(resp.read(128 * 1024))
182
+ except urllib.error.URLError as e:
183
+ raise RuntimeError(f"Failed to fetch pubkey from {url}: {e}") from e
184
+ pk = doc.get("pubkey", {})
185
+ return _parse_pem(pk.get("kid", ""), pk.get("pem", ""))
186
+
187
+
188
+ def _parse_pem(kid: str, pem_str: str) -> dict:
189
+ if "REPLACE_WITH_ACTUAL" in pem_str:
190
+ raise ValueError("Placeholder public key detected — use set_pubkey_url() or set_pubkey_pem() with the real key")
191
+ from cryptography.hazmat.primitives.serialization import load_pem_public_key
192
+ pub_key = load_pem_public_key(pem_str.encode())
193
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
194
+ if not isinstance(pub_key, RSAPublicKey):
195
+ raise ValueError("Public key is not RSA")
196
+ if pub_key.key_size < 2048:
197
+ raise ValueError(f"Public key too small ({pub_key.key_size} bits)")
198
+ # Compute fingerprint from DER bytes.
199
+ der = pub_key.public_bytes(
200
+ encoding=serialization.Encoding.DER,
201
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
202
+ )
203
+ fp = hashlib.sha256(der).hexdigest()
204
+ return {"kid": kid, "key": pub_key, "fingerprint": fp}
205
+
206
+
207
+ def _check_fingerprint(loaded: dict, expected: str) -> None:
208
+ expected = expected.lower().replace(":", "").strip()
209
+ if len(expected) < 16:
210
+ raise ValueError("Fingerprint must be at least 16 hex characters")
211
+ if not loaded["fingerprint"].startswith(expected):
212
+ raise ValueError(
213
+ f"Pubkey fingerprint mismatch: expected prefix {expected}, got {loaded['fingerprint']}"
214
+ )
215
+
216
+
217
+ def _normalize_azure_endpoint(raw: str) -> str:
218
+ raw = raw.strip()
219
+ parsed = urlparse(raw)
220
+ if parsed.scheme != "https" or not parsed.netloc:
221
+ raise ValueError(f"Azure endpoint must be https://xxx.azure.com (got {raw!r})")
222
+ host = parsed.netloc.lower()
223
+ host_only = host.rsplit(":", 1)[0] if ":" in host else host
224
+ if not host_only.endswith(".azure.com"):
225
+ raise ValueError(f"Azure endpoint must end with .azure.com (got {host!r})")
226
+ return f"https://{host}"
@@ -0,0 +1,38 @@
1
+ """Channel type slug constants for Poixe channels."""
2
+
3
+
4
+ class ChannelType:
5
+ """All supported Poixe channel types."""
6
+
7
+ OPENAI_CHATGPT = "openai_chatgpt"
8
+ AZURE_CHATGPT = "azure_chatgpt"
9
+ DEEPSEEK_DEEPSEEK = "deepseek_deepseek"
10
+ ANTHROPIC_CLAUDE = "anthropic_claude"
11
+ GOOGLE_AISTUDIO_GEMINI = "google_aistudio_gemini"
12
+ XAI_GROK = "xai_grok"
13
+ VOLCENGINE_DEEPSEEK = "volcengine_deepseek"
14
+ VOLCENGINE_DOUBAO = "volcengine_doubao"
15
+ ALIYUN_DEEPSEEK = "aliyun_deepseek"
16
+ ALIYUN_QWEN = "aliyun_qwen"
17
+ SILICONFLOW_DEEPSEEK = "siliconflow_deepseek"
18
+ SILICONFLOW_QWEN = "siliconflow_qwen"
19
+ CHUTES_DEEPSEEK = "chutes_deepseek"
20
+ CHUTES_QWEN = "chutes_qwen"
21
+ MOONSHOT_KIMI = "moonshot_kimi"
22
+ ALIYUN_KIMI = "aliyun_kimi"
23
+ SILICONFLOW_KIMI = "siliconflow_kimi"
24
+ CHUTES_KIMI = "chutes_kimi"
25
+ CHUTES_CHATGPT = "chutes_chatgpt"
26
+ CEREBRAS_CHATGPT = "cerebras_chatgpt"
27
+ GROQ_CHATGPT = "groq_chatgpt"
28
+ FIREWORKS_CHATGPT = "fireworks_chatgpt"
29
+ NOVITA_CHATGPT = "novita_chatgpt"
30
+ TOGETHER_CHATGPT = "together_chatgpt"
31
+ NEBIUS_CHATGPT = "nebius_chatgpt"
32
+ AWS_BEDROCK_CLAUDE = "aws_bedrock_claude"
33
+ AWS_PLATFORM_CLAUDE = "aws_platform_claude"
34
+ AZURE_CLAUDE = "azure_claude"
35
+ GOOGLE_VERTEX_GEMINI = "google_vertex_gemini"
36
+ OPENROUTER_CHATGPT = "openrouter_chatgpt"
37
+ OPENROUTER_CLAUDE = "openrouter_claude"
38
+ OPENROUTER_GEMINI = "openrouter_gemini"
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "poixe-encrypt"
7
+ version = "0.1.0"
8
+ description = "Client-side API key encryption for Poixe channels"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ keywords = ["poixe", "encryption", "api-key"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ ]
24
+
25
+ dependencies = [
26
+ "cryptography>=41.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/poixeai/poixe-security"
31
+ Repository = "https://github.com/poixeai/poixe-security"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["poixe_encrypt"]