t2em 0.6.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.
- t2em-0.6.0/.gitignore +67 -0
- t2em-0.6.0/PKG-INFO +99 -0
- t2em-0.6.0/README.md +73 -0
- t2em-0.6.0/pyproject.toml +45 -0
- t2em-0.6.0/src/t2em/__init__.py +74 -0
- t2em-0.6.0/src/t2em/_meta.py +8 -0
- t2em-0.6.0/src/t2em/base64url.py +45 -0
- t2em-0.6.0/src/t2em/client.py +313 -0
- t2em-0.6.0/src/t2em/envelope.py +162 -0
- t2em-0.6.0/src/t2em/exceptions.py +54 -0
- t2em-0.6.0/src/t2em/facade.py +93 -0
- t2em-0.6.0/src/t2em/fingerprint.py +42 -0
- t2em-0.6.0/src/t2em/keyfile.py +129 -0
- t2em-0.6.0/src/t2em/keys.py +64 -0
- t2em-0.6.0/src/t2em/mode_a.py +158 -0
- t2em-0.6.0/src/t2em/mode_b.py +58 -0
- t2em-0.6.0/src/t2em/protocol.py +27 -0
- t2em-0.6.0/src/t2em/py.typed +0 -0
- t2em-0.6.0/tests/test_client.py +315 -0
- t2em-0.6.0/tests/test_keyfile.py +120 -0
- t2em-0.6.0/tests/test_user_agent.py +57 -0
- t2em-0.6.0/tests/test_vectors.py +260 -0
t2em-0.6.0/.gitignore
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
lerna-debug.log*
|
|
8
|
+
|
|
9
|
+
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
10
|
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
11
|
+
|
|
12
|
+
# Runtime data
|
|
13
|
+
pids
|
|
14
|
+
*.pid
|
|
15
|
+
*.seed
|
|
16
|
+
*.pid.lock
|
|
17
|
+
|
|
18
|
+
# Coverage directory
|
|
19
|
+
coverage
|
|
20
|
+
*.lcov
|
|
21
|
+
.nyc_output
|
|
22
|
+
|
|
23
|
+
# Dependency directories
|
|
24
|
+
node_modules/
|
|
25
|
+
|
|
26
|
+
# TypeScript cache
|
|
27
|
+
*.tsbuildinfo
|
|
28
|
+
|
|
29
|
+
# Optional npm cache directory
|
|
30
|
+
.npm
|
|
31
|
+
|
|
32
|
+
# Optional eslint cache
|
|
33
|
+
.eslintcache
|
|
34
|
+
|
|
35
|
+
# REPL history
|
|
36
|
+
.node_repl_history
|
|
37
|
+
|
|
38
|
+
# Output of 'npm pack'
|
|
39
|
+
*.tgz
|
|
40
|
+
|
|
41
|
+
# dotenv environment variable files — NEVER commit secrets
|
|
42
|
+
# (Upstash REST tokens, Wrangler secrets live here in development)
|
|
43
|
+
.env
|
|
44
|
+
.env.*
|
|
45
|
+
!.env.example
|
|
46
|
+
|
|
47
|
+
# Build output
|
|
48
|
+
dist
|
|
49
|
+
.output
|
|
50
|
+
|
|
51
|
+
# Wrangler / Cloudflare local state
|
|
52
|
+
.wrangler/
|
|
53
|
+
|
|
54
|
+
# pnpm
|
|
55
|
+
.pnpm-store
|
|
56
|
+
|
|
57
|
+
# Vite files
|
|
58
|
+
vite.config.js.timestamp-*
|
|
59
|
+
vite.config.ts.timestamp-*
|
|
60
|
+
.vite/
|
|
61
|
+
|
|
62
|
+
# NOTE: unlike me2em-protocol, docs/ is NOT ignored here:
|
|
63
|
+
# docs/ holds the source TZ documents (TZ_t2em.md, TZ_we2em.md).
|
|
64
|
+
|
|
65
|
+
# Python bytecode
|
|
66
|
+
__pycache__/
|
|
67
|
+
*.pyc
|
t2em-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: t2em
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Python SDK for t2em.com — zero-knowledge secret drops (Blind Relay) for LangGraph / CrewAI / AutoGen pipelines
|
|
5
|
+
Project-URL: Homepage, https://t2em.com
|
|
6
|
+
Project-URL: Documentation, https://t2em.com/docs/python-sdk.md
|
|
7
|
+
Project-URL: Source, https://github.com/me2em-org/t2em
|
|
8
|
+
Author: Me2eM ecosystem
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: aes-gcm,agents,e2ee,secrets,x25519,zero-knowledge
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: cryptography>=41.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# t2em — Python SDK (PyPI)
|
|
28
|
+
|
|
29
|
+
Python client for [t2em.com](https://t2em.com) for LangGraph / CrewAI /
|
|
30
|
+
AutoGen pipelines (ТЗ `docs/TZ_t2em.md` §8.3).
|
|
31
|
+
|
|
32
|
+
**Status: implemented (Этап 4 done).** Zero-knowledge secret drops —
|
|
33
|
+
the relay stores only an AES-256-GCM envelope and can never derive the
|
|
34
|
+
key.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install t2em # from PyPI (when published)
|
|
40
|
+
pip install -e . # from this repo (editable, dev)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage (the ТЗ §8.3 contract)
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from t2em import T2emClient, generate_x25519_keypair
|
|
47
|
+
|
|
48
|
+
client = T2emClient("https://t2em.com")
|
|
49
|
+
|
|
50
|
+
# Mode B (symmetric — key rides in the URL #fragment, never sent to the server)
|
|
51
|
+
drop = client.push(secret="sk-live-...", ttl=300)
|
|
52
|
+
print(drop.url) # https://t2em.com/s/abc123#xyz789
|
|
53
|
+
print(drop.delete_token)
|
|
54
|
+
secret = client.pull_text(drop.url)
|
|
55
|
+
|
|
56
|
+
# Mode A (asymmetric — sealed to the recipient's X25519 pubkey)
|
|
57
|
+
keys = generate_x25519_keypair()
|
|
58
|
+
drop = client.push(secret="sk-live-...", recipient_pubkey=keys.public_key, ttl=300)
|
|
59
|
+
secret = client.pull_text(drop.url, recipient_privkey=keys.private_key)
|
|
60
|
+
|
|
61
|
+
# Delete before TTL expiry
|
|
62
|
+
client.delete(drop.id, drop.delete_token)
|
|
63
|
+
|
|
64
|
+
# Keyfile — same JSON format as the TS CLI (`t2em-cli keygen`)
|
|
65
|
+
from t2em import write_keyfile, load_keyfile
|
|
66
|
+
pubkey = write_keyfile("./agent.key") # 0600, returns b64url pubkey
|
|
67
|
+
key = load_keyfile("./agent.key") # LoadedKey(private_key, public_key)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Guarantees
|
|
71
|
+
|
|
72
|
+
- Python ≥ 3.10, full type hints (`py.typed`).
|
|
73
|
+
- Networking: stdlib `urllib` only (no `requests`/`httpx`) — §8.3.
|
|
74
|
+
- Crypto: PyCA `cryptography` as the single crypto dependency
|
|
75
|
+
(hand-rolled crypto is forbidden, §11.2); HKDF strictly per RFC 5869.
|
|
76
|
+
- Bit-compatible with the TypeScript protocol `@me2em/protocol`:
|
|
77
|
+
verified against `vectors/t2em-drop.v1.json` (unpack AND deterministic
|
|
78
|
+
re-encryption, both modes) plus TS↔Python cross-pack/unpack checks.
|
|
79
|
+
- Transport is injectable (`T2emClient(base_url, transport=...)`) for
|
|
80
|
+
testing without sockets.
|
|
81
|
+
- Error taxonomy mirrors §4.5: branch on `ProtocolError.code`
|
|
82
|
+
(`MALFORMED` / `UNSUPPORTED_VERSION` / `DECRYPT_FAILED`), `UsageError`,
|
|
83
|
+
`HttpError` (status / code / retry_after).
|
|
84
|
+
|
|
85
|
+
## Layout
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
python-sdk/
|
|
89
|
+
├── src/t2em/ # package (py.typed)
|
|
90
|
+
│ ├── protocol.py # constants: version, TTL bounds, HKDF info (freeze §4.5)
|
|
91
|
+
│ ├── base64url.py # strict RFC 4648 §5 unpadded codec
|
|
92
|
+
│ ├── envelope.py # v1 envelope validation (mirror of @me2em/protocol)
|
|
93
|
+
│ ├── mode_b.py # pack/unpack sym (AES-256-GCM, 12B IV)
|
|
94
|
+
│ ├── mode_a.py # pack/unpack X25519 ECDH + HKDF-SHA256
|
|
95
|
+
│ ├── fingerprint.py # 8-char pubkey fingerprint (§4.2.1)
|
|
96
|
+
│ ├── keyfile.py # t2em-x25519-key-v1 JSON keyfile (0600)
|
|
97
|
+
│ └── client.py # T2emClient: push / pull_text / pull_bytes / delete
|
|
98
|
+
└── tests/ # pytest: vectors bit-compat, client journeys, keyfile
|
|
99
|
+
```
|
t2em-0.6.0/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# t2em — Python SDK (PyPI)
|
|
2
|
+
|
|
3
|
+
Python client for [t2em.com](https://t2em.com) for LangGraph / CrewAI /
|
|
4
|
+
AutoGen pipelines (ТЗ `docs/TZ_t2em.md` §8.3).
|
|
5
|
+
|
|
6
|
+
**Status: implemented (Этап 4 done).** Zero-knowledge secret drops —
|
|
7
|
+
the relay stores only an AES-256-GCM envelope and can never derive the
|
|
8
|
+
key.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install t2em # from PyPI (when published)
|
|
14
|
+
pip install -e . # from this repo (editable, dev)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage (the ТЗ §8.3 contract)
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from t2em import T2emClient, generate_x25519_keypair
|
|
21
|
+
|
|
22
|
+
client = T2emClient("https://t2em.com")
|
|
23
|
+
|
|
24
|
+
# Mode B (symmetric — key rides in the URL #fragment, never sent to the server)
|
|
25
|
+
drop = client.push(secret="sk-live-...", ttl=300)
|
|
26
|
+
print(drop.url) # https://t2em.com/s/abc123#xyz789
|
|
27
|
+
print(drop.delete_token)
|
|
28
|
+
secret = client.pull_text(drop.url)
|
|
29
|
+
|
|
30
|
+
# Mode A (asymmetric — sealed to the recipient's X25519 pubkey)
|
|
31
|
+
keys = generate_x25519_keypair()
|
|
32
|
+
drop = client.push(secret="sk-live-...", recipient_pubkey=keys.public_key, ttl=300)
|
|
33
|
+
secret = client.pull_text(drop.url, recipient_privkey=keys.private_key)
|
|
34
|
+
|
|
35
|
+
# Delete before TTL expiry
|
|
36
|
+
client.delete(drop.id, drop.delete_token)
|
|
37
|
+
|
|
38
|
+
# Keyfile — same JSON format as the TS CLI (`t2em-cli keygen`)
|
|
39
|
+
from t2em import write_keyfile, load_keyfile
|
|
40
|
+
pubkey = write_keyfile("./agent.key") # 0600, returns b64url pubkey
|
|
41
|
+
key = load_keyfile("./agent.key") # LoadedKey(private_key, public_key)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Guarantees
|
|
45
|
+
|
|
46
|
+
- Python ≥ 3.10, full type hints (`py.typed`).
|
|
47
|
+
- Networking: stdlib `urllib` only (no `requests`/`httpx`) — §8.3.
|
|
48
|
+
- Crypto: PyCA `cryptography` as the single crypto dependency
|
|
49
|
+
(hand-rolled crypto is forbidden, §11.2); HKDF strictly per RFC 5869.
|
|
50
|
+
- Bit-compatible with the TypeScript protocol `@me2em/protocol`:
|
|
51
|
+
verified against `vectors/t2em-drop.v1.json` (unpack AND deterministic
|
|
52
|
+
re-encryption, both modes) plus TS↔Python cross-pack/unpack checks.
|
|
53
|
+
- Transport is injectable (`T2emClient(base_url, transport=...)`) for
|
|
54
|
+
testing without sockets.
|
|
55
|
+
- Error taxonomy mirrors §4.5: branch on `ProtocolError.code`
|
|
56
|
+
(`MALFORMED` / `UNSUPPORTED_VERSION` / `DECRYPT_FAILED`), `UsageError`,
|
|
57
|
+
`HttpError` (status / code / retry_after).
|
|
58
|
+
|
|
59
|
+
## Layout
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
python-sdk/
|
|
63
|
+
├── src/t2em/ # package (py.typed)
|
|
64
|
+
│ ├── protocol.py # constants: version, TTL bounds, HKDF info (freeze §4.5)
|
|
65
|
+
│ ├── base64url.py # strict RFC 4648 §5 unpadded codec
|
|
66
|
+
│ ├── envelope.py # v1 envelope validation (mirror of @me2em/protocol)
|
|
67
|
+
│ ├── mode_b.py # pack/unpack sym (AES-256-GCM, 12B IV)
|
|
68
|
+
│ ├── mode_a.py # pack/unpack X25519 ECDH + HKDF-SHA256
|
|
69
|
+
│ ├── fingerprint.py # 8-char pubkey fingerprint (§4.2.1)
|
|
70
|
+
│ ├── keyfile.py # t2em-x25519-key-v1 JSON keyfile (0600)
|
|
71
|
+
│ └── client.py # T2emClient: push / pull_text / pull_bytes / delete
|
|
72
|
+
└── tests/ # pytest: vectors bit-compat, client journeys, keyfile
|
|
73
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "t2em"
|
|
7
|
+
version = "0.6.0"
|
|
8
|
+
description = "Python SDK for t2em.com — zero-knowledge secret drops (Blind Relay) for LangGraph / CrewAI / AutoGen pipelines"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Me2eM ecosystem" }]
|
|
13
|
+
keywords = ["secrets", "zero-knowledge", "e2ee", "agents", "x25519", "aes-gcm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Security :: Cryptography",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
# ТЗ §8.3: zero external NETWORK deps — stdlib urllib only.
|
|
27
|
+
# Crypto: PyCA cryptography as the single crypto dependency (§11.2:
|
|
28
|
+
# hand-rolled crypto is forbidden).
|
|
29
|
+
dependencies = ["cryptography>=41.0"]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=8"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://t2em.com"
|
|
36
|
+
Documentation = "https://t2em.com/docs/python-sdk.md"
|
|
37
|
+
Source = "https://github.com/me2em-org/t2em"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/t2em"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
pythonpath = ["src"]
|
|
45
|
+
addopts = "-q"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""t2em — Python SDK for t2em.com (ТЗ docs/TZ_t2em.md §8.3).
|
|
2
|
+
|
|
3
|
+
Zero-knowledge secret drops over a Blind Relay: the server stores only
|
|
4
|
+
an AES-256-GCM envelope and can never derive the key. Bit-compatible
|
|
5
|
+
with the TypeScript protocol `@t2em/protocol` — verified against
|
|
6
|
+
`vectors/t2em-drop.v1.json`.
|
|
7
|
+
|
|
8
|
+
Typical use::
|
|
9
|
+
|
|
10
|
+
from t2em import T2emClient, generate_x25519_keypair
|
|
11
|
+
|
|
12
|
+
client = T2emClient("https://t2em.com")
|
|
13
|
+
|
|
14
|
+
# Mode B (symmetric, key lives in the URL #fragment)
|
|
15
|
+
drop = client.push(secret="sk-live-...", ttl=300)
|
|
16
|
+
secret = client.pull_text(drop.url)
|
|
17
|
+
|
|
18
|
+
# Mode A (asymmetric, sealed to the recipient's X25519 pubkey)
|
|
19
|
+
keys = generate_x25519_keypair()
|
|
20
|
+
drop = client.push(secret="sk-live-...", recipient_pubkey=keys.public_key, ttl=300)
|
|
21
|
+
secret = client.pull_text(drop.url, recipient_privkey=keys.private_key)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .base64url import b64u_decode, b64u_encode, is_b64u_of_length
|
|
25
|
+
from ._meta import SDK_VERSION, SDK_USER_AGENT
|
|
26
|
+
from .client import CreatedDrop, ShareUrl, T2emClient
|
|
27
|
+
from .envelope import Envelope, ModeAEnvelope, ModeBEnvelope, validate_envelope
|
|
28
|
+
from .exceptions import HttpError, ProtocolError, T2emError, UsageError
|
|
29
|
+
from .facade import pack_drop, unpack_drop
|
|
30
|
+
from .fingerprint import fingerprint_pubkey, generate_fingerprint
|
|
31
|
+
from .keyfile import KEYFILE_KIND, KEYFILE_TYPE, KEYFILE_VERSION, load_keyfile, write_keyfile
|
|
32
|
+
from .keys import X25519KeyPair, generate_x25519_keypair
|
|
33
|
+
from .protocol import (
|
|
34
|
+
HKDF_INFO_DROP_V1,
|
|
35
|
+
MAX_TTL_SECONDS,
|
|
36
|
+
MIN_TTL_SECONDS,
|
|
37
|
+
T2EM_PROTOCOL_VERSION,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__version__ = SDK_VERSION
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"CreatedDrop",
|
|
44
|
+
"Envelope",
|
|
45
|
+
"HKDF_INFO_DROP_V1",
|
|
46
|
+
"KEYFILE_KIND",
|
|
47
|
+
"KEYFILE_TYPE",
|
|
48
|
+
"KEYFILE_VERSION",
|
|
49
|
+
"MAX_TTL_SECONDS",
|
|
50
|
+
"MIN_TTL_SECONDS",
|
|
51
|
+
"ModeAEnvelope",
|
|
52
|
+
"ModeBEnvelope",
|
|
53
|
+
"ProtocolError",
|
|
54
|
+
"HttpError",
|
|
55
|
+
"SDK_USER_AGENT",
|
|
56
|
+
"SDK_VERSION",
|
|
57
|
+
"ShareUrl",
|
|
58
|
+
"T2EM_PROTOCOL_VERSION",
|
|
59
|
+
"T2emClient",
|
|
60
|
+
"T2emError",
|
|
61
|
+
"UsageError",
|
|
62
|
+
"X25519KeyPair",
|
|
63
|
+
"b64u_decode",
|
|
64
|
+
"b64u_encode",
|
|
65
|
+
"fingerprint_pubkey",
|
|
66
|
+
"generate_fingerprint",
|
|
67
|
+
"generate_x25519_keypair",
|
|
68
|
+
"is_b64u_of_length",
|
|
69
|
+
"load_keyfile",
|
|
70
|
+
"pack_drop",
|
|
71
|
+
"unpack_drop",
|
|
72
|
+
"validate_envelope",
|
|
73
|
+
"write_keyfile",
|
|
74
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""SDK identity metadata — single source of the version string."""
|
|
2
|
+
|
|
3
|
+
SDK_VERSION = "0.6.0"
|
|
4
|
+
|
|
5
|
+
#: Identifying User-Agent (Cloudflare browser-integrity checks reject
|
|
6
|
+
#: the bare `Python-urllib` default; the ecosystem is agent-friendly,
|
|
7
|
+
#: so clients identify themselves — ТЗ §5.5, §8.6).
|
|
8
|
+
SDK_USER_AGENT = f"t2em-python-sdk/{SDK_VERSION}"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Base64URL helpers (RFC 4648 §5, no padding) — ТЗ §4.1.
|
|
2
|
+
|
|
3
|
+
Strict mirror of `@t2em/protocol` `base64url.ts`: the same alphabet,
|
|
4
|
+
the same rejection rules, so every wire string produced here is
|
|
5
|
+
byte-identical to the TypeScript implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from .exceptions import ProtocolError
|
|
14
|
+
|
|
15
|
+
_B64URL_ALPHABET = re.compile(r"^[A-Za-z0-9\-_]+$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def b64u_encode(data: bytes) -> str:
|
|
19
|
+
"""Encode bytes to unpadded base64url."""
|
|
20
|
+
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def b64u_decode(text: str) -> bytes:
|
|
24
|
+
"""Throwing strict decoder: rejects any non-base64url alphabet or padding."""
|
|
25
|
+
if not isinstance(text, str) or len(text) == 0:
|
|
26
|
+
raise ProtocolError("MALFORMED", "base64url string must be non-empty")
|
|
27
|
+
if not _B64URL_ALPHABET.match(text):
|
|
28
|
+
raise ProtocolError("MALFORMED", "not base64url (RFC 4648 §5, unpadded)")
|
|
29
|
+
if len(text) % 4 == 1:
|
|
30
|
+
raise ProtocolError("MALFORMED", "invalid base64url length")
|
|
31
|
+
pad = "=" * (-len(text) % 4)
|
|
32
|
+
try:
|
|
33
|
+
return base64.urlsafe_b64decode(text + pad)
|
|
34
|
+
except Exception as exc: # binascii.Error and friends
|
|
35
|
+
raise ProtocolError("MALFORMED", f"invalid base64url: {exc}") from exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_b64u_of_length(text: object, byte_length: int) -> bool:
|
|
39
|
+
"""True when `text` is valid base64url decoding to exactly `byte_length` bytes."""
|
|
40
|
+
if not isinstance(text, str):
|
|
41
|
+
return False
|
|
42
|
+
try:
|
|
43
|
+
return len(b64u_decode(text)) == byte_length
|
|
44
|
+
except ProtocolError:
|
|
45
|
+
return False
|