33# License: BSD-3-Clause
44"""Complete BIP-93 Codex32 implementation"""
55
6- import hashlib
7- import hmac
8- import secrets
9-
106CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
117MS32_CONST = 0x10CE0795C2FD1E62A
128MS32_LONG_CONST = 0x43381E570BF4798AB26
1713
1814
1915def ms32_polymod (values ):
20- GEN = [
16+ """Compute the ms32 polymod."""
17+ gen = [
2118 0x19DC500CE73FDE210 ,
2219 0x1BFAE00DEF77FE529 ,
2320 0x1FBD920FFFE7BEE52 ,
@@ -29,11 +26,12 @@ def ms32_polymod(values):
2926 b = residue >> 60
3027 residue = (residue & 0x0FFFFFFFFFFFFFFF ) << 5 ^ v
3128 for i in range (5 ):
32- residue ^= GEN [i ] if ((b >> i ) & 1 ) else 0
29+ residue ^= gen [i ] if ((b >> i ) & 1 ) else 0
3330 return residue
3431
3532
3633def ms32_verify_checksum (data ):
34+ """Determine long or short checksum and verify it."""
3735 if len (data ) >= 96 : # See Long codex32 Strings
3836 return ms32_verify_long_checksum (data )
3937 if len (data ) <= 93 :
@@ -42,6 +40,7 @@ def ms32_verify_checksum(data):
4240
4341
4442def ms32_create_checksum (data ):
43+ """Determine long or short checksum, create and return it."""
4544 if len (data ) > 80 : # See Long codex32 Strings
4645 return ms32_create_long_checksum (data )
4746 values = data
@@ -50,7 +49,8 @@ def ms32_create_checksum(data):
5049
5150
5251def ms32_long_polymod (values ):
53- GEN = [
52+ """Compute the ms32 long polymod."""
53+ gen = [
5454 0x3D59D273535EA62D897 ,
5555 0x7A9BECB6361C6C51507 ,
5656 0x543F9B7E6C38D8A2A0E ,
@@ -62,21 +62,24 @@ def ms32_long_polymod(values):
6262 b = residue >> 70
6363 residue = (residue & 0x3FFFFFFFFFFFFFFFFF ) << 5 ^ v
6464 for i in range (5 ):
65- residue ^= GEN [i ] if ((b >> i ) & 1 ) else 0
65+ residue ^= gen [i ] if ((b >> i ) & 1 ) else 0
6666 return residue
6767
6868
6969def ms32_verify_long_checksum (data ):
70+ """Verify the long codex32 checksum."""
7071 return ms32_long_polymod (data ) == MS32_LONG_CONST
7172
7273
7374def ms32_create_long_checksum (data ):
75+ """Create the long codex32 checksum."""
7476 values = data
7577 polymod = ms32_long_polymod (values + [0 ] * 15 ) ^ MS32_LONG_CONST
7678 return [(polymod >> 5 * (14 - i )) & 31 for i in range (15 )]
7779
7880
7981def bech32_mul (a , b ):
82+ """Multiply two bech32 values."""
8083 res = 0
8184 for i in range (5 ):
8285 res ^= a if ((b >> i ) & 1 ) else 0
@@ -86,7 +89,8 @@ def bech32_mul(a, b):
8689
8790
8891# noinspection PyPep8
89- def bech32_lagrange (l , x ):
92+ def bech32_lagrange (l , x ): # noqa: E741
93+ """Compute bech32 lagrange."""
9094 n = 1
9195 c = []
9296 for i in l :
@@ -98,18 +102,20 @@ def bech32_lagrange(l, x):
98102 return [bech32_mul (n , bech32_inv [i ]) for i in c ]
99103
100104
101- def ms32_interpolate (l , x ):
105+ def ms32_interpolate (l , x ): # noqa: E741
106+ """Interpolate codex32."""
102107 w = bech32_lagrange ([s [5 ] for s in l ], x )
103108 res = []
104109 for i in range (len (l [0 ])):
105110 n = 0
106- for j in range ( len ( l ) ):
107- n ^= bech32_mul (w [j ], l [ j ] [i ])
111+ for j , val in enumerate ( l ):
112+ n ^= bech32_mul (w [j ], val [i ])
108113 res .append (n )
109114 return res
110115
111116
112- def ms32_recover (l ):
117+ def ms32_recover (l ): # noqa: E741
118+ """Recover the codex32 secret."""
113119 return ms32_interpolate (l , 16 )
114120
115121
@@ -133,8 +139,10 @@ def ms32_recover(l):
133139# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
134140# THE SOFTWARE.
135141
142+ # pylint: disable=missing-class-docstring
136143
137144class Codex32Error (Exception ):
145+ msg = "Base Codex32 error class"
138146 def __init__ (self , extra : str | None = None ):
139147 self .extra = extra
140148 super ().__init__ (extra )
@@ -219,7 +227,7 @@ def bech32_to_u5(bech=''):
219227
220228
221229def bech32_decode (bech = '' , hrp = 'ms' ):
222- """Validate a Bech32/Bech32m string, and determine HRP and data."""
230+ """Validate a Bech32/Codex32 string, and determine HRP and data."""
223231 for i , ch in enumerate (bech ):
224232 if ord (ch ) < 33 or ord (ch ) > 126 :
225233 raise InvalidChar (f"non-printable U+{ ord (ch ):04X} at pos={ i } " )
@@ -237,7 +245,7 @@ def bech32_decode(bech='', hrp='ms'):
237245 return hrp , data
238246
239247
240- def crc (crc_len , values ):
248+ def compute_crc (crc_len , values ):
241249 """Internal function that computes a CRC checksum for padding."""
242250 if not 0 <= crc_len < 5 : # Codex32 string CRC padding
243251 raise InvalidLength (f"{ crc_len !r} (expected int in 0..4)" )
@@ -253,7 +261,7 @@ def crc(crc_len, values):
253261 return crc & (2 ** crc_len - 1 ) # Return last crc_len bits as CRC
254262
255263
256- def convertbits (data , frombits , tobits , pad = True , pad_val = - 1 , verify_pad = False ):
264+ def convertbits (data , frombits , tobits , pad = True , pad_val = - 1 ):
257265 """General power-of-2 base conversion with CRC padding."""
258266 acc = 0
259267 bits = 0
@@ -269,23 +277,25 @@ def convertbits(data, frombits, tobits, pad=True, pad_val=-1, verify_pad=False):
269277 bits -= tobits
270278 ret .append ((acc >> bits ) & maxv )
271279 acc = acc & ((1 << bits ) - 1 )
272- if verify_pad :
273- pad = False
274280 if pad and bits :
275281 if pad_val == - 1 : # Use CRC padding
276282 data_bits = convertbits (ret , tobits , 1 ) + convertbits ([acc ], bits , 1 )
277- pad_val = crc (tobits - bits , convertbits (data_bits , tobits , 1 ))
283+ pad_val = compute_crc (tobits - bits , convertbits (data_bits , tobits , 1 ))
278284 ret .append (((acc << (tobits - bits )) + pad_val ) & maxv )
279285 elif bits >= frombits :
280286 raise IncompleteGroup (f"{ bits } bits left over" )
281- elif verify_pad :
282- if data != convertbits (ret , tobits , frombits , True , pad_val ):
283- pad_str = "CRC" if pad_val < 0 else bin (pad_val )
284- raise InvalidChecksum (f"Padding bits do not match expected { pad_str } padding." )
285287 return ret
286288
289+ def verify_crc (data , pad_val ):
290+ """Verify the codex32 padding matches the specified type."""
291+ unpadded = convertbits (data , 5 , 8 , False )
292+ if data != convertbits (unpadded , 8 , 5 , pad_val = pad_val ):
293+ pad_str = "CRC" if pad_val < 0 else bin (pad_val )
294+ raise InvalidChecksum (f"Padding bits do not match expected { pad_str } padding." )
295+
287296
288297class Codex32String :
298+ """Class representing a Codex32 string."""
289299 def __init__ (self , s = '' ):
290300 self .s = s
291301
@@ -301,20 +311,23 @@ def __hash__(self):
301311 return hash (self .s )
302312
303313 def sanity_check (self ):
314+ """Perform sanity check on the codex32 string."""
304315 parts = self .parts_inner ()
305316 incomplete_group = (len (parts .payload ) * 5 ) % 8
306317 if incomplete_group > 4 :
307318 raise IncompleteGroup (str (incomplete_group ))
308319
309320 @classmethod
310321 def from_unchecksummed_string (cls , s , hrp = "ms" ):
322+ """Create Codex32String from unchecksummed string."""
311323 hrp , data = bech32_decode (s , hrp = hrp )
312324 ret = cls (bech32_encode (data + ms32_create_checksum (data ), hrp ))
313325 ret .sanity_check ()
314326 return ret
315327
316328 @classmethod
317329 def from_string (cls , s , hrp = "ms" ):
330+ """Create Codex32String from a codex32 string."""
318331 _ , data = bech32_decode (s , hrp = hrp )
319332 if not ms32_verify_checksum (data ):
320333 raise InvalidChecksum (f"string={ s } " )
@@ -323,6 +336,7 @@ def from_string(cls, s, hrp="ms"):
323336 return ret
324337
325338 def parts_inner (self ):
339+ """Get inner parts of the codex32 string."""
326340 hrp , s = self .s .rsplit ('1' , 1 ) if '1' in self .s else ("" , self .s )
327341 if len (s ) < 94 and len (s ) > 44 :
328342 checksum_len = 13
@@ -338,7 +352,7 @@ def parts_inner(self):
338352 ret = Parts (
339353 hrp = hrp ,
340354 k = k ,
341- id = s [1 :5 ],
355+ ident = s [1 :5 ],
342356 share_index = s [5 ],
343357 payload = s [6 :len (s ) - checksum_len ],
344358 checksum = s [- checksum_len :],
@@ -348,10 +362,12 @@ def parts_inner(self):
348362 return ret
349363
350364 def parts (self ):
365+ """Get parts of the codex32 string."""
351366 return self .parts_inner ()
352367
353368 @classmethod
354369 def interpolate_at (cls , shares , target ):
370+ """Interpolate to a specific target share index."""
355371 indices = []
356372 ms32_shares = []
357373 s0_parts = shares [0 ].parts ()
@@ -365,54 +381,59 @@ def interpolate_at(cls, shares, target):
365381 raise MismatchedHrp (f"{ s0_parts .hrp } , { parts .hrp } " )
366382 if s0_parts .k != parts .k :
367383 raise MismatchedThreshold (f"{ s0_parts .k } , { parts .k } " )
368- if s0_parts .id != parts .id :
369- raise MismatchedId (f"{ s0_parts .id } , { parts .id } " )
384+ if s0_parts .ident != parts .ident :
385+ raise MismatchedId (f"{ s0_parts .ident } , { parts .ident } " )
370386 if parts .share_index in indices :
371387 raise RepeatedIndex (parts .share_index )
372388 indices .append (parts .share_index )
373389 ms32_shares .append (bech32_decode (share .s )[1 ])
374- for i in range ( len ( shares ) ):
390+ for i , share in enumerate ( shares ):
375391 if indices [i ] == target :
376- return shares [ i ]
392+ return share
377393 result = ms32_interpolate (ms32_shares , CHARSET .index (target .lower ()))
378394 ret = bech32_encode (result , s0_parts .hrp )
379395 return cls (ret )
380396
381397 @classmethod
382- def from_seed (cls , data , hrp = 'ms' , k = 0 , id = '' , share_idx = 's' , pad_val = - 1 ):
398+ # pylint: disable=too-many-positional-arguments,too-many-arguments
399+ def from_seed (cls , data , ident , hrp = 'ms' , k = 0 , share_idx = 's' , pad_val = - 1 ):
400+ """Create Codex32String from seed bytes."""
383401 if 16 > len (data ) or len (data ) > 64 :
384402 raise InvalidLength (f"{ len (data )} bytes data MUST be 16 to 64 bytes" )
385- if len (id ) != 4 :
386- raise IdNotLength4 (f"{ len (id )} " )
403+ if len (ident ) != 4 :
404+ raise IdNotLength4 (f"{ len (ident )} " )
387405 if not (1 < k <= 9 or k == 0 ):
388406 raise InvalidThresholdN (str (k ))
389407 payload = convertbits (data , 8 , 5 , pad_val = pad_val )
390- header = bech32_to_u5 (str (k ) + id + share_idx )
408+ header = bech32_to_u5 (str (k ) + ident + share_idx )
391409 combined = header + payload
392410 ret = bech32_encode (combined + ms32_create_checksum (combined ), hrp )
393411 return cls (ret )
394412
395413class Parts :
396- def __init__ (self , hrp , k , id , share_index , payload , checksum ):
414+ """Class representing parts of a Codex32 string."""
415+ def __init__ (self , hrp , k , ident , share_index , payload , checksum ):
416+ # pylint: disable=too-many-arguments
397417 self .hrp = hrp
398418 self .k = k
399- self .id = id
419+ self .ident = ident
400420 self .share_index = share_index
401421 self .payload = payload
402422 self .checksum = checksum
403423
404424 def data (self ):
425+ """Get data from parts."""
405426 return bytes (convertbits (bech32_to_u5 (self .payload ), 5 , 8 , False ))
406427
407428 def __eq__ (self , other ):
408429 if not isinstance (other , Parts ):
409430 return False
410431 return (self .hrp == other .hrp and
411432 self .k == other .k and
412- self .id == other .id and
433+ self .ident == other .ident and
413434 self .share_index == other .share_index and
414435 self .payload == other .payload and
415436 self .checksum == other .checksum )
416437
417438 def __hash__ (self ):
418- return hash ((self .hrp , self .k , self .id , self .share_index , self .payload , self .checksum ))
439+ return hash ((self .hrp , self .k , self .ident , self .share_index , self .payload , self .checksum ))
0 commit comments