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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any