incy-link-encoder 1.3.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.
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""incy-link-encoder — Python port of @incy/link-encoder.
|
|
2
|
+
|
|
3
|
+
Encode subscription URLs into ``incy://crypt1/<payload>`` deep links
|
|
4
|
+
that the INCY iOS/Android/Desktop clients know how to decode.
|
|
5
|
+
|
|
6
|
+
THIS IS OBFUSCATION, NOT SECURITY. The AES-256-GCM key is derived
|
|
7
|
+
from constants and asset bytes embedded in this package — anyone
|
|
8
|
+
reading the source can reconstruct it. See the README of the JS
|
|
9
|
+
package for the full threat model.
|
|
10
|
+
|
|
11
|
+
Wire format (identical across JS/Python/PHP/Go and the client apps):
|
|
12
|
+
|
|
13
|
+
incy://crypt1/base64url( iv[12] || ciphertext || tag[16] )
|
|
14
|
+
|
|
15
|
+
where the plaintext is compact JSON with sorted keys, e.g.
|
|
16
|
+
``{"n":"Name","url":"https://...","v":1}``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
from base64 import b64decode, urlsafe_b64decode, urlsafe_b64encode
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
|
|
27
|
+
from cryptography.exceptions import InvalidTag
|
|
28
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
29
|
+
|
|
30
|
+
from ._keymat import KEYMAT_A_B64, KEYMAT_B_B64
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"DecryptedLink",
|
|
34
|
+
"KEY_FINGERPRINT",
|
|
35
|
+
"SCHEMES",
|
|
36
|
+
"SCHEME_VERSION",
|
|
37
|
+
"SchemeInfo",
|
|
38
|
+
"VERSION",
|
|
39
|
+
"decrypt_link",
|
|
40
|
+
"encrypt_link",
|
|
41
|
+
"encrypt_link_deterministic",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Salt parts — concatenated in order and fed into the SHA-256 KDF.
|
|
45
|
+
# Split into four constants for the same greppability reasons as in
|
|
46
|
+
# the JS package and the client apps.
|
|
47
|
+
_SALT_P1 = "incy"
|
|
48
|
+
_SALT_P2 = "deep"
|
|
49
|
+
_SALT_P3 = "crypt1"
|
|
50
|
+
_SALT_P4 = "v2026.06"
|
|
51
|
+
|
|
52
|
+
_KEYMAT_A_OFFSET = 1024
|
|
53
|
+
_KEYMAT_B_OFFSET = 2048
|
|
54
|
+
_KEYMAT_LEN = 32
|
|
55
|
+
|
|
56
|
+
_EXPECTED_KEY_FINGERPRINT = (
|
|
57
|
+
"b6bf708471cc90043232967660aade86a50b4e57929db2e53c5fa34db624c08c"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
_SCHEME = "incy"
|
|
61
|
+
_HOST = "crypt1"
|
|
62
|
+
_LINK_PREFIX = f"{_SCHEME}://{_HOST}/"
|
|
63
|
+
|
|
64
|
+
_IV_LEN = 12
|
|
65
|
+
_TAG_LEN = 16
|
|
66
|
+
|
|
67
|
+
VERSION = "1.3.0"
|
|
68
|
+
SCHEME_VERSION = "crypt1"
|
|
69
|
+
KEY_FINGERPRINT = _EXPECTED_KEY_FINGERPRINT
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class SchemeInfo:
|
|
74
|
+
"""One deep-link scheme's identifying constants."""
|
|
75
|
+
|
|
76
|
+
host: str
|
|
77
|
+
prefix: str
|
|
78
|
+
key_fingerprint: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Registry of every deep-link scheme this build understands (today only
|
|
82
|
+
# ``crypt1``). A future key rotation adds ``crypt2`` here while keeping
|
|
83
|
+
# ``crypt1`` so old links never stop decoding. Mirrors ``SCHEMES`` in
|
|
84
|
+
# the JS package.
|
|
85
|
+
SCHEMES: dict[str, SchemeInfo] = {
|
|
86
|
+
"crypt1": SchemeInfo(
|
|
87
|
+
host=_HOST,
|
|
88
|
+
prefix=_LINK_PREFIX,
|
|
89
|
+
key_fingerprint=_EXPECTED_KEY_FINGERPRINT,
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_key_cache: bytes | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _derive_key() -> bytes:
|
|
97
|
+
global _key_cache
|
|
98
|
+
if _key_cache is not None:
|
|
99
|
+
return _key_cache
|
|
100
|
+
a = b64decode(KEYMAT_A_B64)
|
|
101
|
+
b = b64decode(KEYMAT_B_B64)
|
|
102
|
+
if len(a) < _KEYMAT_A_OFFSET + _KEYMAT_LEN or len(b) < _KEYMAT_B_OFFSET + _KEYMAT_LEN:
|
|
103
|
+
raise ValueError("incy-link-encoder: keymat assets are smaller than expected")
|
|
104
|
+
seed = (
|
|
105
|
+
_SALT_P1.encode() + _SALT_P2.encode() + _SALT_P3.encode() + _SALT_P4.encode()
|
|
106
|
+
+ a[_KEYMAT_A_OFFSET : _KEYMAT_A_OFFSET + _KEYMAT_LEN]
|
|
107
|
+
+ b[_KEYMAT_B_OFFSET : _KEYMAT_B_OFFSET + _KEYMAT_LEN]
|
|
108
|
+
)
|
|
109
|
+
key = hashlib.sha256(seed).digest()
|
|
110
|
+
fp = hashlib.sha256(key).hexdigest()
|
|
111
|
+
if fp != _EXPECTED_KEY_FINGERPRINT:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
"incy-link-encoder: derived K1 fingerprint mismatch "
|
|
114
|
+
f"(expected {_EXPECTED_KEY_FINGERPRINT}, got {fp}) — keymat assets are out "
|
|
115
|
+
"of sync with the published clients. Reinstall the package or report a bug."
|
|
116
|
+
)
|
|
117
|
+
_key_cache = key
|
|
118
|
+
return key
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _b64url_encode(data: bytes) -> str:
|
|
122
|
+
return urlsafe_b64encode(data).decode().rstrip("=")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _b64url_decode(s: str) -> bytes:
|
|
126
|
+
pad = "" if len(s) % 4 == 0 else "=" * (4 - len(s) % 4)
|
|
127
|
+
return urlsafe_b64decode(s + pad)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _build_plaintext(url: str, name: str | None) -> bytes:
|
|
131
|
+
if not url or not isinstance(url, str):
|
|
132
|
+
raise TypeError("encrypt_link: url must be a non-empty string")
|
|
133
|
+
payload: dict[str, object] = {"url": url, "v": 1}
|
|
134
|
+
if name:
|
|
135
|
+
payload["n"] = name[:128]
|
|
136
|
+
# Compact JSON with sorted keys — byte-for-byte identical to
|
|
137
|
+
# JSON.stringify over sorted keys in the JS package and to the
|
|
138
|
+
# sorted-keys serializers in the client apps.
|
|
139
|
+
return json.dumps(
|
|
140
|
+
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
141
|
+
).encode("utf-8")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class DecryptedLink:
|
|
146
|
+
"""Result of decrypting an ``incy://crypt1/...`` link."""
|
|
147
|
+
|
|
148
|
+
url: str
|
|
149
|
+
name: str | None = None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def encrypt_link(url: str, name: str | None = None) -> str:
|
|
153
|
+
"""Encrypt a subscription URL into an ``incy://crypt1/<payload>`` link.
|
|
154
|
+
|
|
155
|
+
``name`` is an optional human-readable subscription name shown in
|
|
156
|
+
the receiving app's import sheet (truncated to 128 chars).
|
|
157
|
+
"""
|
|
158
|
+
plaintext = _build_plaintext(url, name)
|
|
159
|
+
key = _derive_key()
|
|
160
|
+
iv = os.urandom(_IV_LEN)
|
|
161
|
+
out = AESGCM(key).encrypt(iv, plaintext, None) # returns ct || tag
|
|
162
|
+
return _LINK_PREFIX + _b64url_encode(iv + out)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def encrypt_link_deterministic(url: str, iv: bytes, name: str | None = None) -> str:
|
|
166
|
+
"""Same as :func:`encrypt_link` with a caller-provided IV.
|
|
167
|
+
|
|
168
|
+
For test vectors only — never reuse an IV across different
|
|
169
|
+
plaintexts in production; that breaks AES-GCM confidentiality.
|
|
170
|
+
"""
|
|
171
|
+
if len(iv) != _IV_LEN:
|
|
172
|
+
raise ValueError("encrypt_link_deterministic: iv must be 12 bytes")
|
|
173
|
+
plaintext = _build_plaintext(url, name)
|
|
174
|
+
key = _derive_key()
|
|
175
|
+
out = AESGCM(key).encrypt(iv, plaintext, None)
|
|
176
|
+
return _LINK_PREFIX + _b64url_encode(iv + out)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def decrypt_link(link: str) -> DecryptedLink:
|
|
180
|
+
"""Decrypt an ``incy://crypt1/<payload>`` link.
|
|
181
|
+
|
|
182
|
+
Raises ``ValueError`` on malformed input or authentication failure
|
|
183
|
+
(wrong key, tampered ciphertext).
|
|
184
|
+
"""
|
|
185
|
+
if not link or not isinstance(link, str):
|
|
186
|
+
raise TypeError("decrypt_link: link must be a non-empty string")
|
|
187
|
+
if not link.startswith(_LINK_PREFIX):
|
|
188
|
+
raise ValueError(f"decrypt_link: expected {_LINK_PREFIX} prefix")
|
|
189
|
+
payload = link[len(_LINK_PREFIX):].rstrip("/")
|
|
190
|
+
if not payload:
|
|
191
|
+
raise ValueError("decrypt_link: empty payload")
|
|
192
|
+
wire = _b64url_decode(payload)
|
|
193
|
+
if len(wire) < _IV_LEN + _TAG_LEN + 1:
|
|
194
|
+
raise ValueError("decrypt_link: payload too short")
|
|
195
|
+
iv, ct_and_tag = wire[:_IV_LEN], wire[_IV_LEN:]
|
|
196
|
+
|
|
197
|
+
key = _derive_key()
|
|
198
|
+
try:
|
|
199
|
+
plaintext = AESGCM(key).decrypt(iv, ct_and_tag, None)
|
|
200
|
+
except InvalidTag:
|
|
201
|
+
raise ValueError("decrypt_link: authentication failed") from None
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
parsed = json.loads(plaintext.decode("utf-8"))
|
|
205
|
+
except (ValueError, UnicodeDecodeError):
|
|
206
|
+
raise ValueError("decrypt_link: malformed plaintext") from None
|
|
207
|
+
url = parsed.get("url") if isinstance(parsed, dict) else None
|
|
208
|
+
if not isinstance(url, str) or not url:
|
|
209
|
+
raise ValueError("decrypt_link: missing url field")
|
|
210
|
+
name = parsed.get("n")
|
|
211
|
+
return DecryptedLink(url=url, name=name if isinstance(name, str) and name else None)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Auto-generated from assets/*.bin. Run `npm run gen-keymat` to refresh.
|
|
2
|
+
KEYMAT_A_B64 = "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"
|
|
3
|
+
KEYMAT_B_B64 = "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"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: incy-link-encoder
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Encode subscription URLs into incy://crypt1/<payload> deep links (Python port of @incy/link-encoder). AES-256-GCM with a key shared with the INCY iOS/Android/Desktop clients.
|
|
5
|
+
Project-URL: Homepage, https://incy.cc/encrypt
|
|
6
|
+
Project-URL: Repository, https://github.com/INCY-DEV/incy-link-encoder
|
|
7
|
+
Author-email: INCY <main@incy.cc>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: aes-gcm,deeplink,encryption,incy,vpn
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: cryptography>=41
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# incy-link-encoder (Python)
|
|
15
|
+
|
|
16
|
+
Python port of [`@incy/link-encoder`](https://github.com/INCY-DEV/incy-link-encoder) —
|
|
17
|
+
encode VPN subscription URLs into `incy://crypt1/<payload>` deep links
|
|
18
|
+
that the INCY iOS, Android, and Desktop clients decode automatically.
|
|
19
|
+
|
|
20
|
+
Wire-compatible with the JS/PHP/Go ports and the client apps; a pinned
|
|
21
|
+
test vector guards against drift.
|
|
22
|
+
|
|
23
|
+
> **This is obfuscation, not security.** The AES-256-GCM key is derived
|
|
24
|
+
> from constants and asset bytes embedded in this package. See the
|
|
25
|
+
> [main README](../README.md) for the full threat model.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install incy-link-encoder
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires Python ≥ 3.9 and [`cryptography`](https://pypi.org/project/cryptography/).
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from incy_link_encoder import encrypt_link, decrypt_link
|
|
39
|
+
|
|
40
|
+
link = encrypt_link("https://sub.your-provider.example/abc123token", name="My Provider VPN")
|
|
41
|
+
print(link)
|
|
42
|
+
# → incy://crypt1/AAECAwQFBgcICQoLNyIQL3rDwRZqnyoD8pGK…
|
|
43
|
+
|
|
44
|
+
decoded = decrypt_link(link)
|
|
45
|
+
print(decoded.url, decoded.name)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
encrypt_link(url: str, name: str | None = None) -> str
|
|
52
|
+
decrypt_link(link: str) -> DecryptedLink # .url, .name
|
|
53
|
+
encrypt_link_deterministic(url: str, iv: bytes, name: str | None = None) -> str # tests only
|
|
54
|
+
|
|
55
|
+
VERSION: str
|
|
56
|
+
SCHEME_VERSION: str # "crypt1"
|
|
57
|
+
KEY_FINGERPRINT: str # SHA-256 of K1
|
|
58
|
+
SCHEMES: dict[str, SchemeInfo] # registry of known schemes (today: crypt1)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Tests
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m unittest discover -s tests -v
|
|
65
|
+
```
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
incy_link_encoder/__init__.py,sha256=9fVCzgkkhsBn2pEeJRUY_kGpqDc15aRZ5DeHE6aGPU0,6897
|
|
2
|
+
incy_link_encoder/_keymat.py,sha256=yS1sWJDF5F3ADXHm7xzSiexiWJbUA06g0G4tW2Xe8w4,11037
|
|
3
|
+
incy_link_encoder-1.3.0.dist-info/METADATA,sha256=90zq6Aa01ZLav5_2fY4T8HhslmfGtwLJMzp1yU8zDNs,2040
|
|
4
|
+
incy_link_encoder-1.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
5
|
+
incy_link_encoder-1.3.0.dist-info/RECORD,,
|