2 TESID: Textualised Encrypted Sequential Identifiers
7 >>> from tesid import TESIDCoder
8 >>> secret_key = '000102030405060708090a0b0c0d0e0f'
9 >>> coder = TESIDCoder(secret_key)
16 >>> coder.encode(2**20 - 1)
18 >>> coder.encode(2**20)
20 >>> coder.encode(2**30 - 1)
22 >>> coder.encode(2**30)
24 >>> coder.encode(2**100 - 1)
25 'ia2bvpjaiju7g5uaxn5t'
26 >>> coder.encode(2**100)
27 Traceback (most recent call last):
29 ValueError: id out of range
30 >>> coder.decode('w2ej')
35 from typing
import List
, NamedTuple
, TypeVar
, Generic
, cast
37 from . import base32
, fpeck
39 __all__
= ['TESIDCoder', 'TypedTESIDCoder']
42 TDiscriminant
= TypeVar('TDiscriminant')
43 class SplitDecode(Generic
[TDiscriminant
]):
44 __slots__
= 'id', 'discriminant'
46 discriminant
: TDiscriminant
48 def __init__(self
, id: int, discriminant
: TDiscriminant
):
50 self
.discriminant
= discriminant
53 return f
'SplitDecode(id={self.id!r}, discriminant={self.discriminant!r})'
55 def __eq__(self
, other
):
56 return self
.id == other
.id and self
.discriminant
== other
.discriminant
63 >>> from tesid import TESIDCoder
64 >>> coder = TESIDCoder('000102030405060708090a0b0c0d0e0f')
66 And for tagging, defining constants is good practice (though look at
67 ``TypedTESIDCoder`` if you’re doing this kind of discrimination):
69 >>> TYPE_SPARSITY = 256 # meaning up to 256 possible types
74 (Methods’ examples start with this foundation.)
77 expanded_key
: List
[int]
79 def __init__(self
, key
: str):
81 Initialise a TESID coder.
83 The key string must be made up of exactly 32 lowercase hexadecimal
84 (0-9a-f) characters, and should have been generated randomly.
85 Refer to external documentation for information on key generation.
87 if key
.isupper() or len(key
) != 32:
88 raise ValueError('key must be 32 lowercase hex characters')
90 self
.expanded_key
= fpeck
.expand(int(key
, 16))
92 def encode(self
, id: int, *, sparsity
: int = 1, discriminant
: int = 0) -> str:
96 Raises ValueError if ``id * sparsity + discriminant``
97 is not in the range [0, 2¹⁰⁰).
102 You can use sparsity and discriminant for things like type tagging:
104 >>> coder.encode(0, sparsity=TYPE_SPARSITY, discriminant=TYPE_A)
106 >>> coder.encode(0, sparsity=TYPE_SPARSITY, discriminant=TYPE_B)
108 >>> coder.encode(0, sparsity=TYPE_SPARSITY, discriminant=TYPE_C)
110 >>> coder.encode(1, sparsity=TYPE_SPARSITY, discriminant=TYPE_A)
112 >>> coder.encode(1, sparsity=TYPE_SPARSITY, discriminant=TYPE_B)
114 >>> coder.encode(1, sparsity=TYPE_SPARSITY, discriminant=TYPE_C)
118 id = id * sparsity
+ discriminant
120 i
= (None if id < 0 else
129 8 if id < 2**100 else None)
132 raise ValueError('id out of range')
134 return base32
.encode(
135 fpeck
.encrypt(self
.expanded_key
, (i
+ 2) * 5, id),
139 def decode(self
, tesid
: str, *, sparsity
: int = 1, discriminant
: int = 0) -> int:
143 Raises ValueError if anything goes wrong.
145 >>> coder.decode('w2ej')
147 >>> coder.decode('invalid')
148 Traceback (most recent call last):
150 ValueError: invalid TESID (wrong length)
152 If sparsity and/or discriminant were used on encode, matching values
153 must be provided here, or else it will fail to decode:
155 >>> coder.decode('w2ej', sparsity=TYPE_SPARSITY, discriminant=TYPE_A)
157 >>> coder.decode('w2ej', sparsity=TYPE_SPARSITY, discriminant=TYPE_C)
158 Traceback (most recent call last):
160 ValueError: invalid TESID (wrong discriminant or sparsity)
163 n
= (len(tesid
) // 2) * 5
164 if len(tesid
) % 2 or not (10 <= n
<= 50):
165 raise ValueError('invalid TESID (wrong length)')
166 id = base32
.decode(tesid
)
168 raise ValueError('invalid TESID (wrong characters)')
169 id = fpeck
.decrypt(self
.expanded_key
, n
, id)
171 # Overly-long-encoding range check
172 if n
> 10 and id < 2**(n
* 2 - 10):
173 raise ValueError('invalid TESID (overly long encoding)')
176 if id < 0 or id % sparsity
:
177 raise ValueError('invalid TESID (wrong discriminant or sparsity)')
178 return id // sparsity
180 def split_decode(self
, tesid
: str, sparsity
: int) -> SplitDecode
[int]:
182 Decode an ID that was encoded with certain sparsity,
183 separating the discriminant and returning it alongside the ID.
185 This is useful if you want to accept various discriminants;
186 one simple use case is better error reporting:
187 “that’s an ID for type A, but this takes IDs for type B”.
189 This allows *you* to identify the discriminant,
190 but due to the encryption, anyone who has only the ID cannot;
191 if you want users to be able to discern the discriminant,
192 consider adding a human-friendly prefix to the ID;
193 I like a single uppercase letter or a word followed by an underscore.
195 This requires that the discriminant be less than the sparsity,
196 or incorrect values will be produced.
200 >>> coder.split_decode('w2ej', TYPE_SPARSITY)
201 SplitDecode(id=0, discriminant=0)
202 >>> coder.split_decode('w6um', TYPE_SPARSITY)
203 SplitDecode(id=0, discriminant=1)
204 >>> coder.split_decode('x45g', TYPE_SPARSITY)
205 SplitDecode(id=0, discriminant=2)
206 >>> coder.split_decode('dh2h', TYPE_SPARSITY)
207 SplitDecode(id=1, discriminant=0)
208 >>> coder.split_decode('a6xy', TYPE_SPARSITY)
209 SplitDecode(id=1, discriminant=1)
212 >>> coder.split_decode('7xgj', TYPE_SPARSITY)
213 SplitDecode(id=1, discriminant=2)
218 id = self
.decode(tesid
)
219 return SplitDecode(id=id // sparsity
, discriminant
=id % sparsity
)
222 TTypeEnum
= TypeVar('TTypeEnum', bound
=Enum
)
223 class TypedTESIDCoder(Generic
[TTypeEnum
]):
225 A TESID coder with type discrimination baked in.
227 >>> from tesid import TypedTESIDCoder, TESIDCoder
228 >>> from enum import Enum
229 >>> class Type(Enum):
233 >>> coder = TypedTESIDCoder(TESIDCoder('000102030405060708090a0b0c0d0e0f'), 256, Type)
235 (Methods’ examples start with this foundation.)
238 def __init__(self
, coder
: TESIDCoder
, sparsity
: int, type_enum
: type[TTypeEnum
]):
240 Initialise a typed TESID coder.
242 This takes a ``TESIDCoder`` (rather than a key) so that you can share a
243 coder, if you don’t always use the one sparsity and type enum.
245 ``sparsity`` must exceed the highest variant in ``type_enum``.
248 self
.sparsity
= sparsity
249 self
.type = type_enum
252 return f
'TypedTESIDCoder(coder={self.coder!r}, sparsity={self.sparsity!r}, type={self.type!r})'
254 def encode(self
, type: TTypeEnum
, id: int):
256 Encode an ID and type.
258 >>> coder.encode(Type.A, 0)
260 >>> coder.encode(Type.B, 0)
262 >>> coder.encode(Type.A, 1)
266 return self
.coder
.encode(id, sparsity
=self
.sparsity
, discriminant
=type.value
)
268 def decode(self
, type: TTypeEnum
, tesid
: str):
270 Decode an ID and type.
272 >>> coder.decode(Type.A, 'w2ej')
274 >>> coder.decode(Type.B, 'w6um')
276 >>> coder.decode(Type.A, 'dh2h')
278 >>> coder.decode(Type.A, 'w6um')
279 Traceback (most recent call last):
281 ValueError: invalid TESID (wrong discriminant or sparsity)
284 return self
.coder
.decode(tesid
, sparsity
=self
.sparsity
, discriminant
=type.value
)
286 def split_decode(self
, tesid
: str) -> SplitDecode
[TTypeEnum
]:
288 Decode an ID but separate and return its discriminant too.
290 >>> coder.split_decode('w2ej')
291 SplitDecode(id=0, discriminant=<Type.A: 0>)
292 >>> coder.split_decode('w6um')
293 SplitDecode(id=0, discriminant=<Type.B: 1>)
294 >>> coder.split_decode('x45g')
295 SplitDecode(id=0, discriminant=<Type.C: 2>)
296 >>> coder.split_decode('dh2h')
297 SplitDecode(id=1, discriminant=<Type.A: 0>)
298 >>> coder.split_decode('a6xy')
299 SplitDecode(id=1, discriminant=<Type.B: 1>)
302 >>> coder.split_decode('7xgj')
303 SplitDecode(id=1, discriminant=<Type.C: 2>)
304 >>> _.discriminant == Type.C
306 >>> coder.split_decode('6mqv') # id=0, discriminant=3
307 Traceback (most recent call last):
309 ValueError: 3 is not a valid Type
312 with_int
= self
.coder
.split_decode(tesid
, sparsity
=self
.sparsity
)
313 with_typed
= cast(SplitDecode
[TTypeEnum
], with_int
)
314 with_typed
.discriminant
= self
.type(with_int
.discriminant
)