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.
- poixe_encrypt-0.1.0/.gitignore +28 -0
- poixe_encrypt-0.1.0/PKG-INFO +78 -0
- poixe_encrypt-0.1.0/README.md +57 -0
- poixe_encrypt-0.1.0/examples/example.py +45 -0
- poixe_encrypt-0.1.0/poixe_encrypt/__init__.py +12 -0
- poixe_encrypt-0.1.0/poixe_encrypt/encrypt.py +226 -0
- poixe_encrypt-0.1.0/poixe_encrypt/types.py +38 -0
- poixe_encrypt-0.1.0/pyproject.toml +34 -0
|
@@ -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"]
|