├── .gitignore ├── README.md ├── form.ui ├── foundation ├── __init__.py ├── bytewords.py ├── cbor_lite.py ├── constants.py ├── crc32.py ├── fountain_decoder.py ├── fountain_encoder.py ├── fountain_utils.py ├── random_sampler.py ├── ur.py ├── ur_decoder.py ├── ur_encoder.py ├── utils.py └── xoshiro256.py ├── qr_type.py ├── requirements.txt ├── screenshot.png └── seedqreader.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | venv/ 4 | config 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SeedQReader 2 | --- 3 | 4 | This project is no longer maintained. Please use this [fork](https://github.com/tadeubas/SeedQReader) instead, which is actively maintained. 5 | -------------------------------------------------------------------------------- /form.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 0 8 | 0 9 | 811 10 | 650 11 | 12 | 13 | 14 | Read 15 | 16 | 17 | 0 18 | 19 | 20 | 21 | Read 22 | 23 | 24 | 25 | 26 | 190 27 | 10 28 | 400 29 | 300 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 10 40 | 350 41 | 771 42 | 201 43 | 44 | 45 | 46 | QPlainTextEdit::WidgetWidth 47 | 48 | 49 | true 50 | 51 | 52 | 53 | 54 | 55 | 190 56 | 320 57 | 401 58 | 20 59 | 60 | 61 | 62 | 0 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 10 72 | 320 73 | 161 74 | 27 75 | 76 | 77 | 78 | Start Read 79 | 80 | 81 | 82 | 83 | 84 | 10 85 | 30 86 | 111 87 | 27 88 | 89 | 90 | 91 | 92 | 93 | 94 | 20 95 | 10 96 | 91 97 | 17 98 | 99 | 100 | 101 | Camera: 102 | 103 | 104 | 105 | 106 | 107 | 90 108 | 3 109 | 31 110 | 27 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | Send 121 | 122 | 123 | 124 | 125 | 10 126 | 10 127 | 781 128 | 81 129 | 130 | 131 | 132 | 133 | 134 | 135 | 250 136 | 140 137 | 450 138 | 450 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 10 149 | 100 150 | 97 151 | 27 152 | 153 | 154 | 155 | Generate 156 | 157 | 158 | 159 | 160 | 161 | 380 162 | 100 163 | 291 164 | 28 165 | 166 | 167 | 168 | 10 169 | 170 | 171 | 500 172 | 173 | 174 | 10 175 | 176 | 177 | 100 178 | 179 | 180 | Qt::Horizontal 181 | 182 | 183 | 15 184 | 185 | 186 | 187 | 188 | 189 | 690 190 | 100 191 | 96 192 | 22 193 | 194 | 195 | 196 | No split 197 | 198 | 199 | 200 | 201 | 202 | 120 203 | 100 204 | 71 205 | 27 206 | 207 | 208 | 209 | Clear 210 | 211 | 212 | 213 | 214 | 215 | 20 216 | 400 217 | 116 218 | 22 219 | 220 | 221 | 222 | Key 1 223 | 224 | 225 | 226 | 227 | 228 | 20 229 | 420 230 | 116 231 | 22 232 | 233 | 234 | 235 | Key 2 236 | 237 | 238 | 239 | 240 | 241 | 20 242 | 440 243 | 116 244 | 22 245 | 246 | 247 | 248 | Key 3 249 | 250 | 251 | 252 | 253 | 254 | 20 255 | 460 256 | 116 257 | 22 258 | 259 | 260 | 261 | Key 4 262 | 263 | 264 | 265 | 266 | 267 | 20 268 | 480 269 | 116 270 | 22 271 | 272 | 273 | 274 | Key 5 275 | 276 | 277 | 278 | 279 | 280 | 200 281 | 100 282 | 71 283 | 27 284 | 285 | 286 | 287 | Save 288 | 289 | 290 | 291 | 292 | 293 | 20 294 | 220 295 | 116 296 | 22 297 | 298 | 299 | 300 | Descriptor 1 301 | 302 | 303 | true 304 | 305 | 306 | 307 | 308 | 309 | 20 310 | 260 311 | 116 312 | 22 313 | 314 | 315 | 316 | Descriptor 3 317 | 318 | 319 | 320 | 321 | 322 | 20 323 | 290 324 | 116 325 | 22 326 | 327 | 328 | 329 | PSBT 1 330 | 331 | 332 | 333 | 334 | 335 | 20 336 | 240 337 | 116 338 | 22 339 | 340 | 341 | 342 | Descriptor 2 343 | 344 | 345 | 346 | 347 | 348 | 20 349 | 310 350 | 116 351 | 22 352 | 353 | 354 | 355 | PSBT 2 356 | 357 | 358 | 359 | 360 | 361 | 20 362 | 330 363 | 116 364 | 22 365 | 366 | 367 | 368 | PSBT 3 369 | 370 | 371 | 372 | 373 | 374 | 20 375 | 350 376 | 116 377 | 22 378 | 379 | 380 | 381 | PSBT 4 382 | 383 | 384 | 385 | 386 | 387 | 20 388 | 370 389 | 116 390 | 22 391 | 392 | 393 | 394 | PSBT 5 395 | 396 | 397 | 398 | 399 | 400 | 10 401 | 180 402 | 111 403 | 27 404 | 405 | 406 | 407 | 408 | 409 | 410 | 10 411 | 140 412 | 111 413 | 27 414 | 415 | 416 | 417 | 418 | 419 | 420 | 440 421 | 590 422 | 67 423 | 17 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 284 434 | 104 435 | 91 436 | 17 437 | 438 | 439 | 440 | Split size: 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | -------------------------------------------------------------------------------- /foundation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythcoiner/SeedQReader/5bda8acb92c21120fd30b324cb38cfdaf81cadbf/foundation/__init__.py -------------------------------------------------------------------------------- /foundation/bytewords.py: -------------------------------------------------------------------------------- 1 | # 2 | # bytewords.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .utils import crc32_bytes, partition 9 | 10 | BYTEWORDS = 'ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabiasbluebodybragbrewbulbbuzzcalmcashcatschefcityclawcodecolacookcostcruxcurlcuspcyandarkdatadaysdelidicedietdoordowndrawdropdrumdulldutyeacheasyechoedgeepicevenexamexiteyesfactfairfernfigsfilmfishfizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgiftgirlglowgoodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhopehornhutsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowljudojugsjumpjunkjurykeepkenokeptkeyskickkilnkingkitekiwiknoblamblavalazyleaflegsliarlimplionlistlogoloudloveluaulucklungmainmanymathmazememomenumeowmildmintmissmonknailnavyneednewsnextnoonnotenumbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolposepuffpumapurrquadquizraceramprealredorichroadrockroofrubyruinrunsrustsafesagascarsetssilkskewslotsoapsolosongstubsurfswantacotasktaxitenttiedtimetinytoiltombtoystriptunatwinuglyundouniturgeuservastveryvetovialvibeviewvisavoidvowswallwandwarmwaspwavewaxywebswhatwhenwhizwolfworkyankyawnyellyogayurtzapszerozestzinczonezoom' 11 | WORD_ARRAY = None 12 | 13 | def decode_word(word, word_len): 14 | global WORD_ARRAY 15 | global BYTEWORDS 16 | 17 | if len(word) != word_len: 18 | raise ValueError('Invalid Bytewords.') 19 | 20 | dim = 26 21 | 22 | # Since the first and last letters of each Byteword are unique, 23 | # we can use them as indexes into a two-dimensional lookup table. 24 | # This table is generated lazily. 25 | if WORD_ARRAY == None: 26 | WORD_ARRAY = [-1] * (dim * dim) # create empty array 27 | 28 | for i in range(256): 29 | byteword_offset = i * 4 30 | x = ord(BYTEWORDS[byteword_offset]) - ord('a') 31 | y = ord(BYTEWORDS[byteword_offset + 3]) - ord('a') 32 | array_offset = y * dim + x 33 | WORD_ARRAY[array_offset] = i 34 | 35 | # If the coordinates generated by the first and last letters are out of bounds, 36 | # or the lookup table contains -1 at the coordinates, then the word is not valid. 37 | x = ord(word[0].lower()) - ord('a') 38 | y = ord((word[3 if len(word) == 4 else 1]).lower()) - ord('a') 39 | if not (0 <= x and x < dim and 0 <= y and y < dim): 40 | raise ValueError('Invalid Bytewords.') 41 | 42 | offset = y * dim + x 43 | value = WORD_ARRAY[offset] 44 | if value == -1: 45 | raise ValueError('Invalid Bytewords.') 46 | 47 | # If we're decoding a full four-letter word, verify that the two middle letters are correct. 48 | if len(word) == 4: 49 | byteword_offset = value * 4 50 | c1 = word[1].lower() 51 | c2 = word[2].lower() 52 | if c1 != BYTEWORDS[byteword_offset + 1] or c2 != BYTEWORDS[byteword_offset + 2]: 53 | raise ValueError('Invalid Bytewords.') 54 | 55 | # Successful decode. 56 | return value 57 | 58 | def get_word(index): 59 | byteword_offset = index * 4 60 | return BYTEWORDS[byteword_offset:byteword_offset + 4] 61 | 62 | def get_minimal_word(index): 63 | byteword_offset = index * 4 64 | return BYTEWORDS[byteword_offset] + BYTEWORDS[byteword_offset + 3] 65 | 66 | def encode(buf, separator): 67 | words = [] 68 | for i in range(len(buf)): 69 | byte = buf[i] 70 | words.append(get_word(byte)) 71 | 72 | return separator.join(words) 73 | 74 | def add_crc(buf): 75 | crc_buf = crc32_bytes(buf) 76 | return buf + crc_buf 77 | 78 | def encode_with_separator(buf, separator): 79 | crc_buf = add_crc(buf) 80 | return encode(crc_buf, separator) 81 | 82 | def encode_minimal(buf): 83 | result = '' 84 | 85 | crc_buf = add_crc(buf) 86 | for i in range(len(crc_buf)): 87 | byte = crc_buf[i] 88 | result += get_minimal_word(byte) 89 | 90 | return result 91 | 92 | def decode(s, separator, word_len): 93 | buf = bytearray() 94 | 95 | if word_len == 4: 96 | words = s.split(separator) 97 | else: 98 | words = partition(s, 2) 99 | 100 | for word in words: 101 | buf.append(decode_word(word, word_len)) 102 | 103 | if len(buf) < 5: 104 | raise ValueError('Invalid Bytewords.') 105 | 106 | # Validate checksum 107 | body = buf[0:-4] 108 | body_checksum = buf[-4:] 109 | checksum = crc32_bytes(body) 110 | if checksum != body_checksum: 111 | raise ValueError('Invalid Bytewords.') 112 | 113 | return body 114 | 115 | Bytewords_Style_standard = 1 116 | Bytewords_Style_uri = 2 117 | Bytewords_Style_minimal = 3 118 | 119 | class Bytewords: 120 | @staticmethod 121 | def encode(style, bytes): 122 | if style == Bytewords_Style_standard: 123 | return encode_with_separator(bytes, ' ') 124 | elif style == Bytewords_Style_uri: 125 | return encode_with_separator(bytes, '-') 126 | elif style == Bytewords_Style_minimal: 127 | return encode_minimal(bytes) 128 | else: 129 | assert(False) 130 | 131 | @staticmethod 132 | def decode(style, str): 133 | if style == Bytewords_Style_standard: 134 | return decode(str, ' ', 4) 135 | elif style == Bytewords_Style_uri: 136 | return decode(str, '-', 4) 137 | elif style == Bytewords_Style_minimal: 138 | return decode(str, 0, 2) 139 | else: 140 | assert(False) 141 | -------------------------------------------------------------------------------- /foundation/cbor_lite.py: -------------------------------------------------------------------------------- 1 | # 2 | # crc32.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | # From: https://bitbucket.org/isode/cbor-lite/raw/6c770624a97e3229e3f200be092c1b9c70a60ef1/include/cbor-lite/codec.h 9 | 10 | # This file is part of CBOR-lite which is copyright Isode Limited 11 | # and others and released under a MIT license. For details, see the 12 | # COPYRIGHT.md file in the top-level folder of the CBOR-lite software 13 | # distribution. 14 | 15 | def bit_length(n): 16 | return len(bin(abs(n))) - 2 17 | 18 | 19 | Flag_None = 0 20 | Flag_Require_Minimal_Encoding = 1 21 | 22 | Tag_Major_unsignedInteger = 0 23 | Tag_Major_negativeInteger = 1 << 5 24 | Tag_Major_byteString = 2 << 5 25 | Tag_Major_textString = 3 << 5 26 | Tag_Major_array = 4 << 5 27 | Tag_Major_map = 5 << 5 28 | Tag_Major_semantic = 6 << 5 29 | Tag_Major_floatingPoint = 7 << 5 30 | Tag_Major_simple = 7 << 5 31 | Tag_Major_mask = 0xe0 32 | 33 | Tag_Minor_length1 = 24 34 | Tag_Minor_length2 = 25 35 | Tag_Minor_length4 = 26 36 | Tag_Minor_length8 = 27 37 | 38 | Tag_Minor_false = 20 39 | Tag_Minor_true = 21 40 | Tag_Minor_null = 22 41 | Tag_Minor_undefined = 23 42 | Tag_Minor_half_float = 25 43 | Tag_Minor_singleFloat = 26 44 | Tag_Minor_doubleFloat = 27 45 | 46 | Tag_Minor_dateTime = 0 47 | Tag_Minor_epochDateTime = 1 48 | Tag_Minor_positiveBignum = 2 49 | Tag_Minor_negativeBignum = 3 50 | Tag_Minor_decimalFraction = 4 51 | Tag_Minor_bigFloat = 5 52 | Tag_Minor_convertBase64Url = 21 53 | Tag_Minor_convertBase64 = 22 54 | Tag_Minor_convertBase16 = 23 55 | Tag_Minor_cborEncodedData = 24 56 | Tag_Minor_uri = 32 57 | Tag_Minor_base64Url = 33 58 | Tag_Minor_base64 = 34 59 | Tag_Minor_regex = 35 60 | Tag_Minor_mimeMessage = 36 61 | Tag_Minor_selfDescribeCbor = 55799 62 | Tag_Minor_mask = 0x1f 63 | Tag_Undefined = Tag_Major_semantic + Tag_Minor_undefined 64 | 65 | 66 | def get_byte_length(value): 67 | if value < 24: 68 | return 0 69 | 70 | return (bit_length(value) + 7) // 8 71 | 72 | class CBOREncoder: 73 | def __init__(self): 74 | self.buf = bytearray() 75 | 76 | def get_bytes(self): 77 | return self.buf 78 | 79 | def encodeTagAndAdditional(self, tag, additional): 80 | self.buf.append(tag + additional) 81 | return 1 82 | 83 | def encodeTagAndValue(self, tag, value): 84 | length = get_byte_length(value) 85 | 86 | # 5-8 bytes required, use 8 bytes 87 | if length >= 5 and length <= 8: 88 | self.encodeTagAndAdditional(tag, Tag_Minor_length8) 89 | self.buf.append((value >> 56) & 0xff) 90 | self.buf.append((value >> 48) & 0xff) 91 | self.buf.append((value >> 40) & 0xff) 92 | self.buf.append((value >> 32) & 0xff) 93 | self.buf.append((value >> 24) & 0xff) 94 | self.buf.append((value >> 16) & 0xff) 95 | self.buf.append((value >> 8) & 0xff) 96 | self.buf.append(value & 0xff) 97 | 98 | # 3-4 bytes required, use 4 bytes 99 | elif length == 3 or length == 4: 100 | self.encodeTagAndAdditional(tag, Tag_Minor_length4) 101 | self.buf.append((value >> 24) & 0xff) 102 | self.buf.append((value >> 16) & 0xff) 103 | self.buf.append((value >> 8) & 0xff) 104 | self.buf.append(value & 0xff) 105 | 106 | elif length == 2: 107 | self.encodeTagAndAdditional(tag, Tag_Minor_length2) 108 | self.buf.append((value >> 8) & 0xff) 109 | self.buf.append(value & 0xff) 110 | 111 | elif length == 1: 112 | self.encodeTagAndAdditional(tag, Tag_Minor_length1) 113 | self.buf.append(value & 0xff) 114 | 115 | elif length == 0: 116 | self.encodeTagAndAdditional(tag, value) 117 | 118 | else: 119 | raise Exception("Unsupported byte length of {} for value in encodeTagAndValue()".format(length)) 120 | 121 | encoded_size = 1 + length 122 | return encoded_size 123 | 124 | def encodeUnsigned(self, value): 125 | return self.encodeTagAndValue(Tag_Major_unsignedInteger, value) 126 | 127 | def encodeNegative(self, value): 128 | return self.encodeTagAndValue(Tag_Major_negativeInteger, value) 129 | 130 | def encodeInteger(self, value): 131 | if value >= 0: 132 | return self.encodeUnsigned(value) 133 | else: 134 | return self.encodeNegative(value) 135 | 136 | def encodeBool(self, value): 137 | return self.encodeTagAndValue(Tag_Major_simple, Tag_Minor_true if value else Tag_Minor_false) 138 | 139 | def encodeBytes(self, value): 140 | length = self.encodeTagAndValue(Tag_Major_byteString, len(value)) 141 | self.buf += value 142 | return length + len(value) 143 | 144 | def encodeEncodedBytesPrefix(self, value): 145 | length = self.encodeTagAndValue(Tag_Major_semantic, Tag_Minor_cborEncodedData) 146 | return length + self.encodeTagAndAdditional 147 | 148 | def encodeEncodedBytes(self, value): 149 | length = self.encodeTagAndValue(Tag_Major_semantic, Tag_Minor_cborEncodedData) 150 | return length + self.encodeBytes(value) 151 | 152 | def encodeText(self, value): 153 | str_len = len(value) 154 | length = self.encodeTagAndValue(Tag_Major_textString, str_len) 155 | self.buf.append(bytes(value, 'utf8')) 156 | return length + str_len 157 | 158 | def encodeArraySize(self, value): 159 | return self.encodeTagAndValue(Tag_Major_array, value) 160 | 161 | def encodeMapSize(self, value): 162 | return self.encodeTagAndValue(Tag_Major_map, value) 163 | 164 | 165 | class CBORDecoder: 166 | def __init__(self, buf): 167 | self.buf = buf 168 | self.pos = 0 169 | 170 | def decodeTagAndAdditional(self, flags=Flag_None): 171 | if self.pos == len(self.buf): 172 | raise Exception("Not enough input") 173 | octet = self.buf[self.pos] 174 | self.pos += 1 175 | tag = octet & Tag_Major_mask 176 | additional = octet & Tag_Minor_mask 177 | return (tag, additional, 1) 178 | 179 | def decodeTagAndValue(self, flags): 180 | end = len(self.buf) 181 | 182 | if self.pos == end: 183 | raise Exception("Not enough input") 184 | 185 | (tag, additional, length) = self.decodeTagAndAdditional(flags) 186 | if additional < Tag_Minor_length1: 187 | value = additional 188 | return (tag, value, length) 189 | 190 | value = 0 191 | if additional == Tag_Minor_length8: 192 | if end - self.pos < 8: 193 | raise Exception("Not enough input") 194 | for shift in [56, 48, 40, 32, 24, 16, 8, 0]: 195 | value |= self.buf[self.pos] << shift 196 | self.pos += 1 197 | if ((flags & Flag_Require_Minimal_Encoding) and value == 0): 198 | raise Exception("Encoding not minimal") 199 | return (tag, value, self.pos) 200 | elif additional == Tag_Minor_length4: 201 | if end - self.pos < 4: 202 | raise Exception("Not enough input") 203 | for shift in [24, 16, 8, 0]: 204 | value |= self.buf[self.pos] << shift 205 | self.pos += 1 206 | if ((flags & Flag_Require_Minimal_Encoding) and value == 0): 207 | raise Exception("Encoding not minimal") 208 | return (tag, value, self.pos) 209 | elif additional == Tag_Minor_length2: 210 | if end - self.pos < 2: 211 | raise Exception("Not enough input") 212 | for shift in [8, 0]: 213 | value |= self.buf[self.pos] << shift 214 | self.pos += 1 215 | if ((flags & Flag_Require_Minimal_Encoding) and value == 0): 216 | raise Exception("Encoding not minimal") 217 | return (tag, value, self.pos) 218 | elif additional == Tag_Minor_length1: 219 | if end - self.pos < 1: 220 | raise Exception("Not enough input") 221 | value |= self.buf[self.pos] 222 | self.pos += 1 223 | if ((flags & Flag_Require_Minimal_Encoding) and value == 0): 224 | raise Exception("Encoding not minimal") 225 | return (tag, value, self.pos) 226 | 227 | raise Exception("Bad additional value") 228 | 229 | def decodeUnsigned(self, flags=Flag_None): 230 | (tag, value, length) = self.decodeTagAndValue(flags) 231 | if tag != Tag_Major_unsignedInteger: 232 | raise Exception("Expected Tag_Major_unsignedInteger ({}), but found {}".format(Tag_Major_unsignedInteger, tag)) 233 | return (value, length) 234 | 235 | def decodeNegative(self, flags=Flag_None): 236 | (tag, value, length) = self.decodeTagAndValue(flags) 237 | if tag != Tag_Major_negativeInteger: 238 | raise Exception("Expected Tag_Major_negativeInteger, but found {}".format(tag)) 239 | return (value, length) 240 | 241 | def decodeInteger(self, flags=Flag_None): 242 | (tag, value, length) = self.decodeTagAndValue(flags) 243 | if tag == Tag_Major_unsignedInteger: 244 | return (value, length) 245 | elif tag == Tag_Major_negativeInteger: 246 | return (-1 - value, length) # TODO: Check that this is the right way -- do we need to use struct.unpack()? 247 | 248 | def decodeBool(self, flags=Flag_None): 249 | (tag, value, length) = self.decodeTagAndValue(flags) 250 | if tag == Tag_Major_simple: 251 | if value == Tag_Minor_true: 252 | return (True, length) 253 | elif value == Tag_Minor_false: 254 | return (False, length) 255 | raise Exception("Not a Boolean") 256 | raise Exception("Not Simple/Boolean") 257 | 258 | def decodeBytes(self, flags=Flag_None): 259 | # First value is the length of the bytes that follow 260 | (tag, byte_length, size_length) = self.decodeTagAndValue(flags) 261 | if tag != Tag_Major_byteString: 262 | raise Exception("Not a byteString") 263 | 264 | end = len(self.buf) 265 | if end - self.pos < byte_length: 266 | raise Exception("Not enough input") 267 | 268 | value = bytes(self.buf[self.pos : self.pos + byte_length]) 269 | self.pos += byte_length 270 | return (value, size_length + byte_length) 271 | 272 | def decodeEncodedBytesPrefix(self, flags=Flag_None): 273 | (tag, value, length1) = self.decodeTagAndValue(flags) 274 | if tag != Tag_Major_semantic or value != Tag_Minor_cborEncodedData: 275 | raise Exception("Not CBOR Encoded Data") 276 | 277 | (tag, value, length2) = self.decodeTagAndValue(flags) 278 | if tag != Tag_Major_byteString: 279 | raise Exception("Not byteString") 280 | 281 | return (tag, value, length1 + length2) 282 | 283 | def decodeEncodedBytes(self, flags=Flag_None): 284 | (tag, minor_tag, tag_length) = self.decodeTagAndValue(flags) 285 | if tag != Tag_Major_semantic or minor_tag != Tag_Minor_cborEncodedData: 286 | raise Exception("Not CBOR Encoded Data") 287 | 288 | (value, length) = self.decodeBytes(flags) 289 | return (value, tag_length + length) 290 | 291 | def decodeText(self, flags=Flag_None): 292 | # First value is the length of the bytes that follow 293 | (tag, byte_length, size_length) = self.decodeTagAndValue(flags) 294 | if tag != Tag_Major_textString: 295 | raise Exception("Not a textString") 296 | 297 | end = len(self.buf) 298 | if end - self.pos < byte_length: 299 | raise Exception("Not enough input") 300 | 301 | value = bytes(self.buf[self.pos : self.pos + byte_length]) 302 | self.pos += byte_length 303 | return (value, size_length + byte_length) 304 | 305 | def decodeArraySize(self, flags=Flag_None): 306 | (tag, value, length) = self.decodeTagAndValue(flags) 307 | 308 | if tag != Tag_Major_array: 309 | raise Exception("Expected Tag_Major_array, but found {}".format(tag)) 310 | return (value, length) 311 | 312 | def decodeMapSize(self, flags=Flag_None): 313 | (tag, value, length) = self.decodeTagAndValue(flags) 314 | if tag != Tag_Major_mask: 315 | raise Exception("Expected Tag_Major_map, but found {}".format(tag)) 316 | return (value, length) 317 | -------------------------------------------------------------------------------- /foundation/constants.py: -------------------------------------------------------------------------------- 1 | # 2 | # constants.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | MAX_UINT32 = 0xffffffff 9 | MAX_UINT64 = 0xffffffffffffffff 10 | -------------------------------------------------------------------------------- /foundation/crc32.py: -------------------------------------------------------------------------------- 1 | # 2 | # crc32.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .constants import MAX_UINT32 9 | 10 | def bit_length(n): 11 | return len(bin(abs(n))) - 2 12 | 13 | TABLE = None 14 | 15 | def crc32(buf): 16 | # Lazily instantiate CRC table 17 | global TABLE 18 | if TABLE == None: 19 | TABLE = [None] * (256 * 4) 20 | 21 | for i in range(256): 22 | c = i 23 | for j in range(8): 24 | c = (c >> 1) if (c % 2 == 0) else (0xEDB88320 ^ (c >> 1)) 25 | 26 | TABLE[i] = c 27 | 28 | crc = MAX_UINT32 & ~0 29 | for byte in buf: 30 | crc = (crc >> 8) ^ TABLE[(crc ^ byte) & 0xFF] 31 | 32 | return MAX_UINT32 & ~crc 33 | 34 | def crc32n(buf): 35 | n = crc32(buf) 36 | return n.to_bytes(4, 'big') 37 | -------------------------------------------------------------------------------- /foundation/fountain_decoder.py: -------------------------------------------------------------------------------- 1 | # 2 | # fountain_decoder.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .fountain_utils import choose_fragments, contains, is_strict_subset, set_difference 9 | from .utils import join_lists, join_bytes, crc32_int, xor_with, take_first 10 | 11 | class InvalidPart(Exception): 12 | pass 13 | 14 | class InvalidChecksum(Exception): 15 | pass 16 | 17 | class FountainDecoder: 18 | class Part: 19 | def __init__(self, indexes, data): 20 | self.indexes = frozenset(indexes) 21 | self.data = data 22 | 23 | @classmethod 24 | def from_encoder_part(cls, p): 25 | return cls(choose_fragments(p.seq_num, p.seq_len, p.checksum), p.data[:]) 26 | 27 | def indexes(self): 28 | return self.indexes 29 | 30 | def data(self): 31 | return self.data 32 | 33 | def is_simple(self): 34 | return len(self.indexes) == 1 35 | 36 | def index(self): 37 | # TODO: Not efficient 38 | return list(self.indexes)[0] 39 | 40 | # FountainDecoder 41 | def __init__(self): 42 | self.received_part_indexes = set() 43 | self.last_part_indexes = None 44 | self.processed_parts_count = 0 45 | self.result = None 46 | self.expected_part_indexes = None 47 | self.expected_fragment_len = None 48 | self.expected_message_len = None 49 | self.expected_checksum = None 50 | self.simple_parts = {} 51 | self.mixed_parts = {} 52 | self.queued_parts = [] 53 | 54 | def expected_part_count(self): 55 | return len(self.expected_part_indexes) # TODO: Handle None? 56 | 57 | def is_success(self): 58 | result = self.result 59 | return result if not isinstance(result, Exception) else False 60 | 61 | def is_failure(self): 62 | result = self.result 63 | return result if isinstance(result, Exception) else False 64 | 65 | def is_complete(self): 66 | return self.result != None 67 | 68 | def result_message(self): 69 | return self.result 70 | 71 | def result_error(self): 72 | return self.result 73 | 74 | def estimated_percent_complete(self): 75 | if self.is_complete(): 76 | return 1 77 | if self.expected_part_indexes == None: 78 | return 0 79 | estimated_input_parts = self.expected_part_count() * 1.75 80 | return min(0.99, self.processed_parts_count / estimated_input_parts) 81 | 82 | def receive_part(self, encoder_part): 83 | # Don't process the part if we're already done 84 | if self.is_complete(): 85 | return False 86 | 87 | # Don't continue if this part doesn't validate 88 | if not self.validate_part(encoder_part): 89 | return False 90 | 91 | # Add this part to the queue 92 | p = FountainDecoder.Part.from_encoder_part(encoder_part) 93 | self.last_part_indexes = p.indexes 94 | self.enqueue(p) 95 | 96 | # Process the queue until we're done or the queue is empty 97 | while not self.is_complete() and len(self.queued_parts) != 0: 98 | self.process_queue_item() 99 | 100 | # Keep track of how many parts we've processed 101 | self.processed_parts_count += 1 102 | 103 | # self.print_part_end() 104 | 105 | return True 106 | 107 | # Join all the fragments of a message together, throwing away any padding 108 | @staticmethod 109 | def join_fragments(fragments, message_len): 110 | message = join_bytes(fragments) 111 | return take_first(message, message_len) 112 | 113 | def enqueue(self, p): 114 | self.queued_parts.append(p) 115 | 116 | def process_queue_item(self): 117 | part = self.queued_parts.pop(0) 118 | # self.print_part(part) 119 | 120 | if part.is_simple(): 121 | self.process_simple_part(part) 122 | else: 123 | self.process_mixed_part(part) 124 | # self.print_state() 125 | 126 | def reduce_mixed_by(self, p): 127 | # Reduce all the current mixed parts by the given part 128 | reduced_parts = [] 129 | for value in self.mixed_parts.values(): 130 | reduced_parts.append(self.reduce_part_by_part(value, p)) 131 | 132 | # Collect all the remaining mixed parts 133 | new_mixed = {} 134 | for reduced_part in reduced_parts: 135 | # If this reduced part is now simple 136 | if reduced_part.is_simple(): 137 | # Add it to the queue 138 | self.enqueue(reduced_part) 139 | else: 140 | # Otherwise, add it to the dict of current mixed parts 141 | new_mixed[reduced_part.indexes] = reduced_part 142 | 143 | self.mixed_parts = new_mixed 144 | 145 | def reduce_part_by_part(self, a, b): 146 | # If the fragments mixed into `b` are a strict (proper) subset of those in `a`... 147 | if is_strict_subset(b.indexes, a.indexes): 148 | # The new fragments in the revised part are `a` - `b`. 149 | new_indexes = set_difference(a.indexes, b.indexes) 150 | # The new data in the revised part are `a` XOR `b` 151 | new_data = xor_with(bytearray(a.data), b.data) 152 | return self.Part(new_indexes, new_data) 153 | else: 154 | # `a` is not reducable by `b`, so return a 155 | return a 156 | 157 | def process_simple_part(self, p): 158 | # Don't process duplicate parts 159 | fragment_index = p.index() 160 | if contains(self.received_part_indexes, fragment_index): 161 | return 162 | 163 | # Record this part 164 | self.simple_parts[p.indexes] = p 165 | self.received_part_indexes.add(fragment_index) 166 | 167 | # If we've received all the parts 168 | if self.received_part_indexes == self.expected_part_indexes: 169 | # Reassemble the message from its fragments 170 | sorted_parts = [] 171 | for value in self.simple_parts.values(): 172 | sorted_parts.append(value) 173 | 174 | sorted_parts.sort(key=lambda a: a.index()) 175 | 176 | fragments = [] 177 | for part in sorted_parts: 178 | fragments.append(part.data) 179 | 180 | message = self.join_fragments(fragments, self.expected_message_len) 181 | 182 | # Verify the message checksum and note success or failure 183 | checksum = crc32_int(message) 184 | if(checksum == self.expected_checksum): 185 | self.result = bytes(message) 186 | else: 187 | self.result = InvalidChecksum() 188 | 189 | else: 190 | # Reduce all the mixed parts by this part 191 | self.reduce_mixed_by(p) 192 | 193 | def process_mixed_part(self, p): 194 | # Don't process duplicate parts 195 | for r in self.mixed_parts.values(): 196 | if r == p.indexes: 197 | return 198 | 199 | # Reduce this part by all the others 200 | p2 = p # TODO: Does this need to make a copy of p? 201 | for r in self.simple_parts.values(): 202 | p2 = self.reduce_part_by_part(p2, r) 203 | 204 | for r in self.mixed_parts.values(): 205 | p2 = self.reduce_part_by_part(p2, r) 206 | 207 | # If the part is now simple 208 | if p2.is_simple(): 209 | # Add it to the queue 210 | self.enqueue(p2) 211 | else: 212 | # Reduce all the mixed parts by this one 213 | self.reduce_mixed_by(p2) 214 | # Record this new mixed part 215 | self.mixed_parts[p2.indexes] = p2 216 | 217 | def validate_part(self, p): 218 | # If this is the first part we've seen 219 | if self.expected_part_indexes == None: 220 | # Record the things that all the other parts we see will have to match to be valid. 221 | self.expected_part_indexes = set() 222 | for i in range(p.seq_len): 223 | self.expected_part_indexes.add(i) 224 | 225 | self.expected_message_len = p.message_len 226 | self.expected_checksum = p.checksum 227 | self.expected_fragment_len = len(p.data) 228 | else: 229 | # If this part's values don't match the first part's values, throw away the part 230 | if self.expected_part_count() != p.seq_len: 231 | return False 232 | if self.expected_message_len != p.message_len: 233 | return False 234 | if self.expected_checksum != p.checksum: 235 | return False 236 | if self.expected_fragment_len != len(p.data): 237 | return False 238 | 239 | # This part should be processed 240 | return True 241 | 242 | # debugging 243 | def indexes_to_string(self, indexes): 244 | i = list(indexes) 245 | i.sort() 246 | s = [str(j) for j in i] 247 | return '[{}]'.format(', '.join(s)) 248 | 249 | def result_description(self): 250 | if self.result == None: 251 | return 'None' 252 | 253 | if self.is_success(): 254 | return '{} bytes'.format(len(self.result)) 255 | elif self.is_failure(): 256 | return 'Exception: {}'.format(self.result) 257 | else: 258 | assert(False) 259 | 260 | def print_part(self, p): 261 | print('part indexes: {}'.format(self.indexes_to_string(p.indexes))) 262 | 263 | def print_part_end(self): 264 | expected = self.expected_part_count() if self.expected_part_indexes != None else 'None' 265 | percent = int(round(self.estimated_percent_complete() * 100)) 266 | print("processed: {}, expected: {}, received: {}, percent: {}%".format(self.processed_parts_count, expected, len(self.received_part_indexes), percent)) 267 | 268 | def print_state(self): 269 | parts = self.expected_part_count() if self.expected_part_indexes != None else 'None' 270 | received = self.indexes_to_string(self.received_part_indexes) 271 | mixed = [] 272 | for indexes, p in self.mixed_parts.items(): 273 | mixed.append(self.indexes_to_string(indexes)) 274 | 275 | mixed_s = "[{}]".format(', '.join(mixed)) 276 | queued = len(self.queued_parts) 277 | res = self.result_description() 278 | print('parts: {}, received: {}, mixed: {}, queued: {}, result: {}'.format(parts, received, mixed_s, queued, res)) 279 | -------------------------------------------------------------------------------- /foundation/fountain_encoder.py: -------------------------------------------------------------------------------- 1 | # 2 | # fountain_encoder.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | import math 9 | from .cbor_lite import CBORDecoder, CBOREncoder 10 | from .fountain_utils import choose_fragments 11 | from .utils import split, crc32_int, xor_into, data_to_hex 12 | from .constants import MAX_UINT32, MAX_UINT64 13 | 14 | class InvalidHeader(Exception): 15 | pass 16 | 17 | class Part: 18 | 19 | def __init__(self, seq_num, seq_len, message_len, checksum, data): 20 | self.seq_num = seq_num 21 | self.seq_len = seq_len 22 | self.message_len = message_len 23 | self.checksum = checksum 24 | self.data = data 25 | 26 | @staticmethod 27 | def from_cbor(cbor_buf): 28 | try: 29 | decoder = CBORDecoder(cbor_buf) 30 | (array_size, _) = decoder.decodeArraySize() 31 | if array_size != 5: 32 | raise InvalidHeader() 33 | 34 | (seq_num, _) = decoder.decodeUnsigned() 35 | if seq_num > MAX_UINT64: # TODO: Do something better with this check 36 | raise InvalidHeader() 37 | 38 | (seq_len, _) = decoder.decodeUnsigned() 39 | if seq_len > MAX_UINT64: 40 | raise InvalidHeader() 41 | 42 | (message_len, _) = decoder.decodeUnsigned() 43 | if message_len > MAX_UINT64: 44 | raise InvalidHeader() 45 | 46 | (checksum, _) = decoder.decodeUnsigned() 47 | if checksum > MAX_UINT64: 48 | raise InvalidHeader() 49 | 50 | (data, _) = decoder.decodeBytes() 51 | 52 | return Part(seq_num, seq_len, message_len, checksum, data) 53 | except Exception as err: 54 | raise InvalidHeader() 55 | 56 | def cbor(self): 57 | encoder = CBOREncoder() 58 | encoder.encodeArraySize(5) 59 | encoder.encodeInteger(self.seq_num) 60 | encoder.encodeInteger(self.seq_len) 61 | encoder.encodeInteger(self.message_len) 62 | encoder.encodeInteger(self.checksum) 63 | encoder.encodeBytes(self.data) 64 | return encoder.get_bytes() 65 | 66 | def seq_num(self): 67 | return self.seq_num 68 | 69 | def seq_len(self): 70 | return self.seq_len 71 | 72 | def message_len(self): 73 | return self.message_len 74 | 75 | def checksum(self): 76 | return self.checksum 77 | 78 | def data(self): 79 | return self.data 80 | 81 | def description(self): 82 | return "seqNum:{}, seqLen:{}, messageLen:{}, checksum:{}, data:{}".format( 83 | self.seq_num, self.seq_len, self.message_len, self.checksum, data_to_hex(self.data)) 84 | 85 | class FountainEncoder: 86 | def __init__(self, message, max_fragment_len, first_seq_num = 0, min_fragment_len = 10): 87 | assert(len(message) <= MAX_UINT32) 88 | self.message_len = len(message) 89 | self.checksum = crc32_int(message) 90 | self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len) 91 | self.fragments = FountainEncoder.partition_message(message, self.fragment_len) 92 | self.seq_num = first_seq_num 93 | 94 | @staticmethod 95 | def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len): 96 | assert(message_len > 0) 97 | assert(min_fragment_len > 0) 98 | assert(max_fragment_len >= min_fragment_len) 99 | max_fragment_count = message_len // min_fragment_len 100 | fragment_len = None 101 | 102 | for fragment_count in range(1, max_fragment_count + 1): 103 | fragment_len = math.ceil(message_len / fragment_count) 104 | if fragment_len <= max_fragment_len: 105 | break 106 | 107 | assert(fragment_len != None) 108 | return fragment_len 109 | 110 | 111 | @staticmethod 112 | def partition_message(message, fragment_len): 113 | remaining = message 114 | fragments = [] 115 | while len(remaining) != 0: 116 | (fragment, remaining) = split(remaining, fragment_len) 117 | padding = fragment_len - len(fragment) 118 | while padding > 0: 119 | fragment.append(0) 120 | padding -= 1 121 | fragments.append(fragment) 122 | 123 | return fragments 124 | 125 | def last_part_indexes(self): 126 | return self.last_part_indexes 127 | 128 | def seq_len(self): 129 | return len(self.fragments) 130 | 131 | # This becomes `true` when the minimum number of parts 132 | # to relay the complete message have been generated 133 | def is_complete(self): 134 | return self.seq_num >= self.seq_len() 135 | 136 | # True if only a single part will be generated. 137 | def is_single_part(self): 138 | return self.seq_len() == 1 139 | 140 | def next_part(self): 141 | self.seq_num += 1 142 | self.seq_num = self.seq_num % MAX_UINT32 # wrap at period 2^32 143 | indexes = choose_fragments(self.seq_num, self.seq_len(), self.checksum) 144 | mixed = self.mix(indexes) 145 | data = bytes(mixed) 146 | return Part(self.seq_num, self.seq_len(), self.message_len, self.checksum, data) 147 | 148 | def mix(self, indexes): 149 | result = [0] * self.fragment_len 150 | for index in indexes: 151 | xor_into(result, self.fragments[index]) 152 | return result 153 | -------------------------------------------------------------------------------- /foundation/fountain_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # fountain_utils.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .random_sampler import RandomSampler 9 | from .utils import int_to_bytes 10 | from .xoshiro256 import Xoshiro256 11 | 12 | # Fisher-Yates shuffle 13 | def shuffled(items, rng): 14 | remaining = items 15 | result = [] 16 | while len(remaining) > 0: 17 | index = rng.next_int(0, len(remaining) - 1) 18 | item = remaining.pop(index) 19 | result.append(item) 20 | 21 | return result 22 | 23 | def choose_degree(seq_len, rng): 24 | degree_probabilities = [] 25 | for i in range(1, seq_len + 1): 26 | degree_probabilities.append(1.0 / i) 27 | 28 | degree_chooser = RandomSampler(degree_probabilities) 29 | return degree_chooser.next(lambda: rng.next_double()) + 1 30 | 31 | def choose_fragments(seq_num, seq_len, checksum): 32 | # The first `seq_len` parts are the "pure" fragments, not mixed with any 33 | # others. This means that if you only generate the first `seq_len` parts, 34 | # then you have all the parts you need to decode the message. 35 | if seq_num <= seq_len: 36 | return set([seq_num - 1]) 37 | else: 38 | seed = int_to_bytes(seq_num) + int_to_bytes(checksum) 39 | rng = Xoshiro256.from_bytes(seed) 40 | degree = choose_degree(seq_len, rng) 41 | indexes = [] 42 | 43 | for i in range(seq_len): 44 | indexes.append(i) 45 | shuffled_indexes = shuffled(indexes, rng) 46 | return set(shuffled_indexes[0:degree]) 47 | 48 | def contains(set_or_list, el): 49 | return el in set_or_list 50 | 51 | def is_strict_subset(a, b): 52 | return a.issubset(b) 53 | 54 | def set_difference(a, b): 55 | return a.difference(b) -------------------------------------------------------------------------------- /foundation/random_sampler.py: -------------------------------------------------------------------------------- 1 | # 2 | # random_sampler.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | class RandomSampler: 9 | 10 | def __init__(self, probs): 11 | for p in probs: 12 | assert(p > 0) 13 | 14 | # Normalize given probabilities 15 | total = sum(probs) 16 | assert(total > 0) 17 | 18 | n = len(probs) 19 | 20 | P = [] 21 | for p in probs: 22 | P.append((p * float(n)) / total) 23 | 24 | S = [] 25 | L = [] 26 | 27 | # Set separate index lists for small and large probabilities: 28 | for i in reversed(range(0, n)): 29 | # at variance from Schwarz, we reverse the index order 30 | if P[i] < 1: 31 | S.append(i) 32 | else: 33 | L.append(i) 34 | 35 | # Work through index lists 36 | _probs = [0] * n 37 | _aliases = [0] * n 38 | 39 | while len(S) > 0 and len(L) > 0: 40 | a = S.pop() # Schwarz's l 41 | g = L.pop() # Schwarz's g 42 | _probs[a] = P[a] 43 | _aliases[a] = g 44 | P[g] += P[a] - 1 45 | if P[g] < 1: 46 | S.append(g) 47 | else: 48 | L.append(g) 49 | 50 | while len(L) > 0: 51 | _probs[L.pop()] = 1 52 | 53 | while len(S) > 0: 54 | # can only happen through numeric instability 55 | _probs[S.pop()] = 1 56 | 57 | self.probs = _probs 58 | self.aliases = _aliases 59 | 60 | def next(self, rng_func): 61 | r1 = rng_func() 62 | r2 = rng_func() 63 | n = len(self.probs) 64 | i = int(float(n) * r1) 65 | return i if r2 < self.probs[i] else self.aliases[i] 66 | -------------------------------------------------------------------------------- /foundation/ur.py: -------------------------------------------------------------------------------- 1 | # 2 | # ur.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .utils import is_ur_type 9 | 10 | class InvalidType(Exception): 11 | pass 12 | 13 | class UR: 14 | 15 | def __init__(self, type, cbor): 16 | if not is_ur_type(type): 17 | raise InvalidType() 18 | 19 | self.type = type 20 | self.cbor = cbor 21 | 22 | def __eq__(self, obj): 23 | if obj == None: 24 | return False 25 | return self.type == obj.type and self.cbor == obj.cbor 26 | -------------------------------------------------------------------------------- /foundation/ur_decoder.py: -------------------------------------------------------------------------------- 1 | # 2 | # ur_decoder.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .ur import UR 9 | from .fountain_encoder import FountainEncoder, Part as FountainEncoderPart 10 | from .fountain_decoder import FountainDecoder 11 | from .bytewords import * 12 | from .utils import drop_first, is_ur_type 13 | 14 | class InvalidScheme(Exception): 15 | pass 16 | 17 | class InvalidType(Exception): 18 | pass 19 | 20 | class InvalidPathLength(Exception): 21 | pass 22 | 23 | class InvalidSequenceComponent(Exception): 24 | pass 25 | 26 | class InvalidFragment(Exception): 27 | pass 28 | 29 | class URDecoder: 30 | def __init__(self): 31 | self.fountain_decoder = FountainDecoder() 32 | self.expected_type = None 33 | self.result = None 34 | 35 | @staticmethod 36 | def decode(str): 37 | (type, components) = URDecoder.parse(str) 38 | if len(components) == 0: 39 | raise InvalidPathLength() 40 | 41 | body = components[0] 42 | return URDecoder.decode_by_type(type, body) 43 | 44 | @staticmethod 45 | def decode_by_type(type, body): 46 | cbor = Bytewords.decode(Bytewords_Style_minimal, body) 47 | return UR(type, cbor) 48 | 49 | @staticmethod 50 | def parse(str): 51 | # Don't consider case 52 | lowered = str.lower() 53 | 54 | # Validate URI scheme 55 | if not lowered.startswith('ur:'): 56 | raise InvalidScheme() 57 | 58 | path = drop_first(lowered, 3) 59 | 60 | # Split the remainder into path components 61 | components = path.split('/') 62 | 63 | # Make sure there are at least two path components 64 | if len(components) < 2: 65 | raise InvalidPathLength() 66 | 67 | # Validate the type 68 | type = components[0] 69 | if not is_ur_type(type): 70 | raise InvalidType() 71 | 72 | comps = components[1:] # Don't include the ur type 73 | return (type, comps) 74 | 75 | @staticmethod 76 | def parse_sequence_component(str): 77 | try: 78 | comps = str.split('-') 79 | if len(comps) != 2: 80 | raise InvalidSequenceComponent() 81 | seq_num = int(comps[0]) 82 | seq_len = int(comps[1]) 83 | if seq_num < 1 or seq_len < 1: 84 | raise InvalidSequenceComponent() 85 | return (seq_num, seq_len) 86 | except: 87 | raise InvalidSequenceComponent() 88 | 89 | def validate_part(self, type): 90 | if self.expected_type == None: 91 | if not is_ur_type(type): 92 | return False 93 | self.expected_type = type 94 | return True 95 | else: 96 | return type == self.expected_type 97 | 98 | def receive_part(self, str): 99 | try: 100 | # Don't process the part if we're already done 101 | if self.result != None: 102 | return False 103 | 104 | # Don't continue if this part doesn't validate 105 | (type, components) = URDecoder.parse(str) 106 | if not self.validate_part(type): 107 | return False 108 | 109 | # If this is a single-part UR then we're done 110 | if len(components) == 1: 111 | body = components[0] 112 | self.result = self.decode_by_type(type, body) 113 | return True 114 | 115 | # Multi-part URs must have two path components: seq/fragment 116 | if len(components) != 2: 117 | raise InvalidPathLength() 118 | seq = components[0] 119 | fragment = components[1] 120 | 121 | # Parse the sequence component and the fragment, and make sure they agree. 122 | (seq_num, seq_len) = URDecoder.parse_sequence_component(seq) 123 | cbor = Bytewords.decode(Bytewords_Style_minimal, fragment) 124 | part = FountainEncoderPart.from_cbor(cbor) 125 | if seq_num != part.seq_num or seq_len != part.seq_len: 126 | return False 127 | 128 | # Process the part 129 | if not self.fountain_decoder.receive_part(part): 130 | return False 131 | 132 | if self.fountain_decoder.is_success(): 133 | self.result = UR(type, self.fountain_decoder.result_message()) 134 | elif self.fountain_decoder.is_failure(): 135 | self.result = self.fountain_decoder.result_error() 136 | 137 | return True 138 | except Exception as err: 139 | return False 140 | 141 | def expected_type(self): 142 | return self.expected_type 143 | 144 | def expected_part_count(self): 145 | return self.fountain_decoder.expected_part_count() 146 | 147 | def received_part_indexes(self): 148 | return self.fountain_decoder.received_part_indexes 149 | 150 | def last_part_indexes(self): 151 | return self.fountain_decoder.last_part_indexes 152 | 153 | def processed_parts_count(self): 154 | return self.fountain_decoder.processed_parts_count 155 | 156 | def estimated_percent_complete(self): 157 | return self.fountain_decoder.estimated_percent_complete() 158 | 159 | def is_success(self): 160 | result = self.result 161 | return result if not isinstance(result, Exception) else False 162 | 163 | def is_failure(self): 164 | result = self.result 165 | return result if isinstance(result, Exception) else False 166 | 167 | def is_complete(self): 168 | return self.result != None 169 | 170 | def result_message(self): 171 | return self.result 172 | 173 | def result_error(self): 174 | return self.result 175 | 176 | -------------------------------------------------------------------------------- /foundation/ur_encoder.py: -------------------------------------------------------------------------------- 1 | # 2 | # ur_encoder.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .fountain_encoder import FountainEncoder 9 | from .bytewords import * 10 | 11 | class UREncoder: 12 | # Start encoding a (possibly) multi-part UR. 13 | def __init__(self, ur, max_fragment_len, first_seq_num = 0, min_fragment_len = 10): 14 | self.ur = ur 15 | self.fountain_encoder = FountainEncoder(ur.cbor, max_fragment_len, first_seq_num, min_fragment_len) 16 | 17 | # Encode a single-part UR. 18 | @staticmethod 19 | def encode(ur): 20 | body = Bytewords.encode(Bytewords_Style_minimal, ur.cbor) 21 | return UREncoder.encode_ur([ur.type, body]) 22 | 23 | def last_part_indexes(self): 24 | return self.fountain_encoder.last_part_indexes() 25 | 26 | # `True` if the minimal number of parts to transmit the message have been 27 | # generated. Parts generated when this is `true` will be fountain codes 28 | # containing various mixes of the part data. 29 | def is_complete(self): 30 | return self.fountain_encoder.is_complete() 31 | 32 | # `True` if this UR can be contained in a single part. If `True`, repeated 33 | # calls to `next_part()` will all return the same single-part UR. 34 | def is_single_part(self): 35 | return self.fountain_encoder.is_single_part() 36 | 37 | def next_part(self): 38 | part = self.fountain_encoder.next_part() 39 | if self.is_single_part(): 40 | return UREncoder.encode(self.ur) 41 | else: 42 | return UREncoder.encode_part(self.ur.type, part) 43 | 44 | @staticmethod 45 | def encode_part(type, part): 46 | seq = '{}-{}'.format(part.seq_num, part.seq_len) 47 | body = Bytewords.encode(Bytewords_Style_minimal, part.cbor()) 48 | result = UREncoder.encode_ur([type, seq, body]) 49 | return result 50 | 51 | @staticmethod 52 | def encode_uri(scheme, path_components): 53 | path = '/'.join(path_components) 54 | return ':'.join([scheme, path]) 55 | 56 | @staticmethod 57 | def encode_ur(path_components): 58 | return UREncoder.encode_uri('ur', path_components) 59 | -------------------------------------------------------------------------------- /foundation/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # utils.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .crc32 import crc32, crc32n 9 | 10 | def crc32_bytes(buf): 11 | checksum = crc32n(buf) 12 | return checksum 13 | 14 | def crc32_int(buf): 15 | return crc32(buf) 16 | 17 | def data_to_hex(buf): 18 | return ''.join('{:02x}'.format(x) for x in buf) 19 | 20 | def int_to_bytes(n): 21 | # return n.to_bytes((n.bit_length() + 7) // 8, 'big') 22 | return n.to_bytes(4, 'big') 23 | 24 | def bytes_to_int(buf): 25 | return int.from_bytes(buf, 'big') 26 | 27 | def string_to_bytes(s): 28 | return bytes(s, 'utf8') 29 | 30 | def is_ur_type(ch): 31 | if 'a' <= ch and ch <= 'z': 32 | return True 33 | if '0' <= ch and ch <= '9': 34 | return True 35 | if ch == '-': 36 | return True 37 | return False 38 | 39 | def partition(s, n): 40 | return [s[i:i+n] for i in range(0, len(s), n)] 41 | 42 | # Split the given sequence into two parts returned in a tuple 43 | # The first entry in the tuple has the first `count` values. 44 | # The second entry in the tuple has the remaining values. 45 | def split(buf, count): 46 | return (buf[0:count], buf[count:]) 47 | 48 | def join_lists(lists): 49 | # return [y for x in lists for y in x] 50 | return sum(lists, []) 51 | 52 | def join_bytes(list_of_ba): 53 | out = bytearray() 54 | for ba in list_of_ba: 55 | out.extend(ba) 56 | return out 57 | 58 | def xor_into(target, source): 59 | count = len(target) 60 | assert(count == len(source)) # Must be the same length 61 | for i in range(count): 62 | target[i] ^= source[i] 63 | 64 | def xor_with(a, b): 65 | target = a 66 | xor_into(target, b) 67 | return target 68 | 69 | def take_first(s, count): 70 | return s[0:count] 71 | 72 | def drop_first(s, count): 73 | return s[count:] 74 | -------------------------------------------------------------------------------- /foundation/xoshiro256.py: -------------------------------------------------------------------------------- 1 | # 2 | # xoshiro256.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | import sys 9 | try: 10 | import uhashlib as hashlib 11 | except: 12 | try: 13 | import hashlib 14 | except: 15 | sys.exit("ERROR: No hashlib or uhashlib implementation found (required for sha256)") 16 | 17 | from .utils import string_to_bytes, int_to_bytes 18 | from .constants import MAX_UINT64 19 | 20 | # Original Info: 21 | # Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) 22 | 23 | # To the extent possible under law, the author has dedicated all copyright 24 | # and related and neighboring rights to this software to the public domain 25 | # worldwide. This software is distributed without any warranty. 26 | 27 | # See . 28 | 29 | # This is xoshiro256** 1.0, one of our all-purpose, rock-solid 30 | # generators. It has excellent (sub-ns) speed, a state (256 bits) that is 31 | # large enough for any parallel application, and it passes all tests we 32 | # are aware of. 33 | 34 | # For generating just floating-point numbers, xoshiro256+ is even faster. 35 | 36 | # The state must be seeded so that it is not everywhere zero. If you have 37 | # a 64-bit seed, we suggest to seed a splitmix64 generator and use its 38 | # output to fill s. 39 | 40 | def rotl(x, k): 41 | return ((x << k) | (x >> (64 - k))) & MAX_UINT64 42 | 43 | JUMP = [ 0x180ec6d33cfd0aba, 0xd5a61266f0c9392c, 0xa9582618e03fc9aa, 0x39abdc4529b1661c ] 44 | LONG_JUMP = [ 0x76e15d3efefdcbbf, 0xc5004e441c522fb3, 0x77710069854ee241, 0x39109bb02acbe635 ] 45 | 46 | class Xoshiro256: 47 | def __init__(self, arr = None): 48 | self.s = [0] * 4 49 | if arr != None: 50 | self.s[0] = arr[0] 51 | self.s[1] = arr[1] 52 | self.s[2] = arr[2] 53 | self.s[3] = arr[3] 54 | 55 | 56 | def _set_s(self, arr): 57 | for i in range(4): 58 | o = i * 8 59 | v = 0 60 | for n in range(8): 61 | v <<= 8 62 | v |= (arr[o + n]) 63 | self.s[i] = v 64 | 65 | def _hash_then_set_s(self, buf): 66 | m = hashlib.sha256() 67 | m.update(buf) 68 | digest = m.digest() 69 | self._set_s(digest) 70 | 71 | @classmethod 72 | def from_int8_array(cls, arr): 73 | x = Xoshiro256() 74 | x._set_s(arr) 75 | return x 76 | 77 | @classmethod 78 | def from_bytes(cls, buf): 79 | x = Xoshiro256() 80 | x._hash_then_set_s(buf) 81 | return x 82 | 83 | @classmethod 84 | def from_crc32(cls, crc32): 85 | x = Xoshiro256() 86 | buf = int_to_bytes(crc32) 87 | x._hash_then_set_s(buf) 88 | return x 89 | 90 | @classmethod 91 | def from_string(cls, s): 92 | x = Xoshiro256() 93 | buf = string_to_bytes(s) 94 | x._hash_then_set_s(buf) 95 | return x 96 | 97 | def next(self): 98 | result = (rotl((self.s[1] * 5) & MAX_UINT64, 7) * 9) & MAX_UINT64 99 | t = (self.s[1] << 17) & MAX_UINT64 100 | 101 | self.s[2] ^= self.s[0] 102 | self.s[3] ^= self.s[1] 103 | self.s[1] ^= self.s[2] 104 | self.s[0] ^= self.s[3] 105 | 106 | self.s[2] ^= t 107 | 108 | self.s[3] = rotl(self.s[3], 45) & MAX_UINT64 109 | 110 | return result 111 | 112 | def next_double(self): 113 | m = float(MAX_UINT64) + 1 114 | nxt = self.next() 115 | return nxt / m 116 | 117 | def next_int(self, low, high): 118 | return int(self.next_double() * (high - low + 1) + low) & MAX_UINT64 119 | 120 | def next_byte(self): 121 | return self.next_int(0, 255) 122 | 123 | def next_data(self, count): 124 | result = bytearray() 125 | for i in range(count): 126 | result.append(self.next_byte()) 127 | return result 128 | 129 | def jump(self): 130 | global JUMP 131 | 132 | s0 = 0 133 | s1 = 0 134 | s2 = 0 135 | s3 = 0 136 | for i in range(len(JUMP)): 137 | for b in range(64): 138 | if JUMP[i] & (1 << b): 139 | s0 ^= self.s[0] 140 | s1 ^= self.s[1] 141 | s2 ^= self.s[2] 142 | s3 ^= self.s[3] 143 | self.next() 144 | 145 | self.s[0] = s0 146 | self.s[1] = s1 147 | self.s[2] = s2 148 | self.s[3] = s3 149 | 150 | def long_jump(self): 151 | global LONG_JUMP 152 | 153 | s0 = 0 154 | s1 = 0 155 | s2 = 0 156 | s3 = 0 157 | for i in range(len(LONG_JUMP)): 158 | for b in range(64): 159 | if LONG_JUMP[i] & (1 << b): 160 | s0 ^= self.s[0] 161 | s1 ^= self.s[1] 162 | s2 ^= self.s[2] 163 | s3 ^= self.s[3] 164 | self.next() 165 | 166 | self.s[0] = s0 167 | self.s[1] = s1 168 | self.s[2] = s2 169 | self.s[3] = s3 170 | -------------------------------------------------------------------------------- /qr_type.py: -------------------------------------------------------------------------------- 1 | 2 | SPECTER = "specter" 3 | UR = "ur" 4 | 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | embit==0.7.0 2 | numpy==1.26.4 3 | opencv-python==4.7.0.72 4 | Pillow==9.5.0 5 | pypng==0.20220715.0 6 | PyYAML==6.0.1 7 | pyzbar==0.1.9 8 | qrcode==7.4.2 9 | typing_extensions==4.11.0 10 | PySide6==6.6.3.1 11 | shiboken6==6.6.3.1 12 | urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a 13 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythcoiner/SeedQReader/5bda8acb92c21120fd30b324cb38cfdaf81cadbf/screenshot.png -------------------------------------------------------------------------------- /seedqreader.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import re 4 | 5 | from dataclasses import dataclass, field 6 | 7 | from pathlib import Path 8 | 9 | from yaml import load, dump 10 | from yaml.loader import SafeLoader as Loader 11 | 12 | from PySide6.QtWidgets import QApplication, QMainWindow 13 | from PySide6.QtGui import QImage, QPixmap, QPalette, QColor 14 | from PySide6.QtCore import Qt, QFile, QThread, Signal 15 | from PySide6.QtUiTools import QUiLoader 16 | from PySide6.QtGui import QTextOption 17 | 18 | from PIL import ImageQt 19 | 20 | from pyzbar import pyzbar 21 | 22 | import qrcode 23 | 24 | import cv2 25 | 26 | import qr_type 27 | 28 | from foundation.ur_decoder import URDecoder 29 | from foundation.ur_encoder import UREncoder 30 | from foundation.ur import UR 31 | 32 | from urtypes.crypto import PSBT as UR_PSBT 33 | from urtypes.crypto import Account, Output 34 | from urtypes.bytes import Bytes 35 | 36 | from embit.psbt import PSBT 37 | 38 | MAX_LEN = 100 39 | QR_DELAY = 400 40 | FILL_COLOR = "#434343" 41 | 42 | def to_str(bin_): 43 | return bin_.decode('utf-8') 44 | 45 | 46 | @dataclass 47 | class QRCode: 48 | data: str = '' 49 | total_sequences: int = 0 50 | sequences_count: int = 0 51 | is_completed: bool = False 52 | qr_type = None 53 | 54 | def append(self, data: str): 55 | self.data_init(1) 56 | self.data = data 57 | self.sequences_count += 1 58 | self.is_completed = True 59 | 60 | def data_init(self, sequences: int): 61 | self.total_sequences = sequences 62 | self.sequences_count = 0 63 | 64 | 65 | @dataclass 66 | class MultiQRCode(QRCode): 67 | data_stack: list = field(default_factory=list) 68 | is_init: bool = False 69 | current: int = 0 70 | total_sequences = None 71 | qr_type = None 72 | data_type = None 73 | decoder = None 74 | encoder = None 75 | 76 | def step(self): 77 | if self.qr_type == qr_type.SPECTER: 78 | self.total_sequences = len(self.data_stack) 79 | 80 | return f"{self.current + 1}/{self.total_sequences}" 81 | 82 | elif self.qr_type == qr_type.UR: 83 | return f"{self.current + 1}/{self.total_sequences}" 84 | 85 | def append(self, data: tuple): 86 | if self.qr_type == qr_type.SPECTER: 87 | self.append_specter(data) 88 | 89 | elif self.qr_type == qr_type.UR: 90 | self.append_ur(data) 91 | 92 | def append_specter(self, data: tuple): 93 | # print(f'MultiQRCode.append({data})') 94 | sequence = data[0] 95 | total_sequences = data[1] 96 | data = data[2] 97 | 98 | if not self.is_init: 99 | self.data_init(total_sequences) 100 | self.is_init = True 101 | 102 | if not self.data_stack[sequence-1]: 103 | self.data_stack[sequence-1] = data 104 | else: 105 | if data != self.data_stack[sequence-1]: 106 | print(f"{data} != {self.data_stack[sequence-1]}") 107 | raise ValueError('Same sequences have different data!') 108 | self.check_complete_specter() 109 | 110 | def append_ur(self, data: tuple): 111 | if not self.decoder: 112 | self.decoder = URDecoder() 113 | 114 | self.decoder.receive_part(data) 115 | 116 | self.check_complete_ur() 117 | 118 | def data_init(self, sequences: int): 119 | super().data_init(sequences) 120 | self.data_stack = [None] * sequences 121 | 122 | def check_complete_specter(self): 123 | fill_sequences = 0 124 | for i in self.data_stack: 125 | if i: 126 | fill_sequences += 1 127 | 128 | self.sequences_count = fill_sequences 129 | 130 | if fill_sequences == self.total_sequences: 131 | self.is_completed = True 132 | data = '' 133 | 134 | for i in self.data_stack: 135 | data += i 136 | self.data = data 137 | 138 | def check_complete_ur(self): 139 | if self.decoder.is_complete(): 140 | if self.decoder.is_success(): 141 | self.is_completed = True 142 | cbor = self.decoder.result_message().cbor 143 | _type = self.decoder.result_message().type 144 | # XPub 145 | if _type == 'crypto-account': 146 | self.data = Account.from_cbor(cbor).output_descriptors[0].descriptor() 147 | # PSBT 148 | elif _type == 'crypto-psbt': 149 | self.data = UR_PSBT.from_cbor(cbor).data 150 | if type(self.data) is bytes: 151 | self.data = PSBT.parse(self.data).to_string() 152 | 153 | # Descriptor 154 | elif _type == 'crypto-output': 155 | self.data = Output.from_cbor(cbor).descriptor() 156 | # bytes 157 | elif _type == 'bytes': 158 | print('bytes') 159 | self.data = Bytes.from_cbor(cbor).data.decode('utf-8') 160 | 161 | else: 162 | print(f"Type not yet implemented: {type}") 163 | return 164 | 165 | print(f"{_type}:{self.data}") 166 | 167 | else: 168 | print("fail to complete UR parsing: ", end='') 169 | print(self.decoder.result_error()) 170 | 171 | @staticmethod 172 | def from_string(data, max=MAX_LEN, type=None, format=None): 173 | 174 | if (max and len(data) > max) or format == 'UR': 175 | out = MultiQRCode() 176 | out.data = data 177 | if format == 'UR': 178 | out.qr_type = qr_type.UR 179 | elif format == 'Specter': 180 | out.qr_type = qr_type.SPECTER 181 | 182 | if format == 'Specter': 183 | while len(data) > max: 184 | sequence = data[:max] 185 | data = data[max:] 186 | out.data_stack.append(sequence) 187 | if len(data): 188 | out.data_stack.append(data) 189 | 190 | out.total_sequences = len(out.data_stack) 191 | out.sequences_count = out.total_sequences 192 | out.is_completed = True 193 | 194 | elif format == 'UR': 195 | _UR = None 196 | if type == 'PSBT': 197 | out.data_type = 'crypto-psbt' 198 | data = PSBT.from_string(data).serialize() 199 | _UR = UR_PSBT 200 | elif type == 'Descriptor': 201 | out.data_type = 'bytes' 202 | _UR = Bytes 203 | elif type == 'Key': 204 | print("key") 205 | out.data_type = 'bytes' 206 | _UR = Bytes 207 | elif type == 'Bytes': 208 | out.data_type = 'bytes' 209 | _UR = Bytes 210 | else: 211 | return 212 | if not max: 213 | max = 100000 214 | ur = UR(out.data_type, _UR(data).to_cbor()) 215 | out.encoder = UREncoder(ur, max) 216 | out.total_sequences = out.encoder.fountain_encoder.seq_len() 217 | 218 | else: 219 | out = QRCode() 220 | out.data = data 221 | out.data_init(1) 222 | 223 | return out 224 | 225 | def next(self) -> str: 226 | if self.qr_type == qr_type.SPECTER: 227 | self.current += 1 228 | if self.current >= self.total_sequences: 229 | self.current = 0 230 | 231 | data = self.data_stack[self.current] 232 | 233 | digit_a = self.current + 1 234 | digit_b = self.total_sequences 235 | 236 | data = f"p{digit_a}of{digit_b} {data}" 237 | print(data) 238 | 239 | return data 240 | 241 | elif self.qr_type == qr_type.UR: 242 | self.current = self.encoder.fountain_encoder.seq_num 243 | data = self.encoder.next_part().upper() 244 | print(data) 245 | return data 246 | 247 | 248 | class ReadQR(QThread): 249 | 250 | data = Signal(object) 251 | video_stream = Signal(object) 252 | 253 | def __init__(self, parent): 254 | QThread.__init__(self) 255 | self.parent = parent 256 | self.finished.connect(self.on_finnish) 257 | self.qr_data: QRCode | MultiQRCode = None 258 | self.capture = None 259 | self.end = False 260 | 261 | def run(self): 262 | self.qr_data: QRCode | MultiQRCode = None 263 | # Initialize the camera 264 | camera_id = self.parent.get_camera_id() 265 | 266 | if camera_id is None: 267 | return 268 | self.capture = cv2.VideoCapture(camera_id) 269 | 270 | self.parent.ui.btn_start_read.setText('Stop') 271 | while not self.end: 272 | self.msleep(30) 273 | 274 | ret, frame = self.capture.read() 275 | 276 | if ret: 277 | # Convert the frame to RGB format 278 | frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 279 | 280 | # Create a QImage from the frame data 281 | height, width, channel = frame.shape 282 | image = QImage(frame.data, width, height, QImage.Format_RGB888) 283 | 284 | # Create a QPixmap from the QImage 285 | pixmap = QPixmap.fromImage(image) 286 | 287 | # Scale the QPixmap to fit the label dimensions 288 | scaled_pixmap = pixmap.scaled(self.parent.ui.video_in.size(), Qt.KeepAspectRatio) 289 | 290 | # Set the pixmap to the label 291 | self.video_stream.emit(scaled_pixmap) 292 | 293 | data = pyzbar.decode(frame) 294 | if data: 295 | try: 296 | self.decode(to_str(data[0].data)) 297 | except Exception as e: 298 | print(e) 299 | 300 | if self.qr_data: 301 | if self.qr_data.is_completed: 302 | self.video_stream.emit(None) 303 | self.data.emit(self.qr_data.data) 304 | if self.qr_data.qr_type is None: 305 | print(f"QRCode:{self.qr_data.data}") 306 | break 307 | if self.end: 308 | self.video_stream.emit(None) 309 | return 310 | 311 | def decode(self, data): 312 | 313 | # Multipart QR Code case 314 | 315 | # specter format 316 | if re.match(r'^p\d+of\d+\s', data, re.IGNORECASE): 317 | 318 | if not self.qr_data: 319 | self.qr_data = MultiQRCode() 320 | self.qr_data.qr_type = qr_type.SPECTER 321 | 322 | header = data.split(' ')[0][1:].split('of') 323 | data = ' '.join(data.split(' ')[1:]) 324 | 325 | digit_a = header[0] 326 | digit_b = header[1] 327 | 328 | self.qr_data.append((int(digit_a), int(digit_b), data)) 329 | 330 | progress = round(self.qr_data.sequences_count / self.qr_data.total_sequences * 100) 331 | self.parent.ui.read_progress.setValue(progress) 332 | self.parent.ui.read_progress.setFormat(f"{self.qr_data.sequences_count}/{self.qr_data.total_sequences}") 333 | self.parent.ui.read_progress.setVisible(True) 334 | 335 | elif re.match(r'^UR:', data, re.IGNORECASE): 336 | 337 | if not self.qr_data: 338 | self.qr_data = MultiQRCode() 339 | self.qr_data.qr_type = qr_type.UR 340 | 341 | self.qr_data.append(data) 342 | 343 | progress = self.qr_data.decoder.estimated_percent_complete() * 100 344 | self.qr_data.total_sequences = self.qr_data.decoder.expected_part_count() 345 | self.qr_data.sequences_count = self.qr_data.decoder.processed_parts_count() 346 | self.parent.ui.read_progress.setValue(progress) 347 | self.parent.ui.read_progress.setFormat(f"{self.qr_data.sequences_count}/{self.qr_data.total_sequences}") 348 | self.parent.ui.read_progress.setVisible(True) 349 | 350 | 351 | else: 352 | self.qr_data = QRCode() 353 | self.qr_data.append(data) 354 | 355 | 356 | 357 | def on_finnish(self): 358 | if self.capture: 359 | self.capture.release() 360 | self.parent.ui.read_progress.setValue(0) 361 | self.parent.ui.read_progress.setVisible(False) 362 | self.parent.ui.read_progress.setFormat('') 363 | self.parent.ui.btn_start_read.setText('Start read') 364 | 365 | 366 | class DisplayQR(QThread): 367 | 368 | video_stream = Signal(object) 369 | 370 | def __init__(self, parent): 371 | QThread.__init__(self) 372 | self.parent = parent 373 | self.qr_data: QRCode | MultiQRCode = None 374 | self.stop = False 375 | 376 | def run(self): 377 | self.stop = False 378 | if self.qr_data.total_sequences > 1 or self.qr_data.qr_type == qr_type.UR: 379 | while not self.stop: 380 | data = self.qr_data.next() 381 | 382 | self.display_qr(data) 383 | self.parent.ui.steps.setText(self.qr_data.step()) 384 | if self.qr_data.total_sequences == 1: 385 | break 386 | if not self.stop: 387 | self.msleep(QR_DELAY) 388 | 389 | if self.qr_data.total_sequences == 1: 390 | while not self.stop: 391 | self.msleep(QR_DELAY) 392 | 393 | self.parent.ui.steps.setText('') 394 | 395 | elif self.qr_data.total_sequences == 1: 396 | data = self.qr_data.data 397 | self.display_qr(data) 398 | while not self.stop: 399 | self.msleep(QR_DELAY) 400 | 401 | def display_qr(self, data): 402 | 403 | qr = qrcode.QRCode() 404 | qr.add_data(data) 405 | qr.make(fit=False) 406 | img = qr.make_image() 407 | pil_image = img.convert("RGB") 408 | qimage = ImageQt.ImageQt(pil_image) 409 | qimage = qimage.convertToFormat(QImage.Format_RGB888) 410 | 411 | # Create a QPixmap from the QImage 412 | pixmap = QPixmap.fromImage(qimage) 413 | 414 | scaled_pixmap = pixmap.scaled(self.parent.ui.video_out.size(), Qt.KeepAspectRatio) 415 | self.video_stream.emit(scaled_pixmap) 416 | 417 | def on_stop(self): 418 | self.video_stream.emit(None) 419 | self.stop = True 420 | 421 | 422 | class MainWindow(QMainWindow): 423 | stop_display = Signal() 424 | 425 | def __init__(self, loader): 426 | super().__init__() 427 | 428 | # Set up the main window 429 | path = os.fspath(Path(__file__).resolve().parent / "form.ui") 430 | ui_file = QFile(path) 431 | ui_file.open(QFile.ReadOnly) 432 | self.ui = loader.load(ui_file, self) 433 | ui_file.close() 434 | self.setWindowTitle("SeedQReader") 435 | self.setFixedSize(812,670) 436 | 437 | self.setCentralWidget(self.ui) 438 | 439 | self.load_config() 440 | 441 | self.ui.btn_start_read.clicked.connect(self.on_qr_read) 442 | self.ui.btn_generate.clicked.connect(self.on_btn_generate) 443 | self.ui.btn_clear.clicked.connect(self.on_btn_clear) 444 | self.ui.send_slider.valueChanged.connect(self.on_slider_move) 445 | 446 | self.ui.data_out.setWordWrapMode(QTextOption.WrapAnywhere) 447 | 448 | # init radio button 449 | 450 | self.ui.desc_1.toggled.connect(self.on_radio_toggled) 451 | self.ui.desc_2.toggled.connect(self.on_radio_toggled) 452 | self.ui.desc_3.toggled.connect(self.on_radio_toggled) 453 | 454 | self.ui.psbt_1.toggled.connect(self.on_radio_toggled) 455 | self.ui.psbt_2.toggled.connect(self.on_radio_toggled) 456 | self.ui.psbt_3.toggled.connect(self.on_radio_toggled) 457 | self.ui.psbt_4.toggled.connect(self.on_radio_toggled) 458 | self.ui.psbt_5.toggled.connect(self.on_radio_toggled) 459 | 460 | self.ui.key_1.toggled.connect(self.on_radio_toggled) 461 | self.ui.key_2.toggled.connect(self.on_radio_toggled) 462 | self.ui.key_3.toggled.connect(self.on_radio_toggled) 463 | self.ui.key_4.toggled.connect(self.on_radio_toggled) 464 | self.ui.key_5.toggled.connect(self.on_radio_toggled) 465 | 466 | self.ui.desc_1.setChecked(True) 467 | self.radio_selected = 'desc_1' 468 | self.on_radio_toggled() 469 | 470 | self.ui.btn_save.clicked.connect(self.on_btn_save) 471 | 472 | self.ui.combo_format.addItems(['Specter', 'UR']) 473 | self.format = self.ui.combo_format.currentText() 474 | self.ui.combo_format.currentIndexChanged.connect(self.on_format_change) 475 | self.ui.combo_type.currentIndexChanged.connect(self.on_data_type_change) 476 | 477 | self.ui.combo_type.addItems(['Descriptor', 'PSBT', 'Key', 'Bytes']) 478 | self.ui.combo_type.hide() 479 | self.data_type = None 480 | 481 | self.ui.btn_camera_update.clicked.connect(self.on_camera_update) 482 | 483 | self.on_slider_move() 484 | self.on_camera_update() 485 | 486 | self.init_qr() 487 | 488 | def init_qr(self): 489 | 490 | self.read_qr = ReadQR(self) 491 | self.read_qr.video_stream.connect(self.upd_camera_stream) 492 | self.read_qr.data.connect(self.on_qr_data_read) 493 | 494 | self.display_qr = DisplayQR(self) 495 | self.display_qr.video_stream.connect(self.on_qr_display) 496 | self.stop_display.connect(self.display_qr.on_stop) 497 | 498 | def load_config(self): 499 | 500 | if not os.path.exists('config'): 501 | f = open('config', 'w') 502 | f.close() 503 | 504 | with open('config', 'r') as f: 505 | data = load(f, Loader=Loader) 506 | 507 | if not data: 508 | data = {} 509 | 510 | self.config = data 511 | 512 | def dump_config(self): 513 | with open('config', 'w') as f: 514 | dump(self.config, f) 515 | 516 | @staticmethod 517 | def list_available_cameras(): 518 | index = 0 519 | available_cameras = [] 520 | while True: 521 | cap = cv2.VideoCapture(index) 522 | if cap.isOpened(): 523 | available_cameras.append(str(index)) 524 | cap.release() 525 | index += 1 526 | else: 527 | if index > 20 and not available_cameras: 528 | break 529 | elif available_cameras and (index - int(available_cameras[-1])) > 2: 530 | break 531 | else: 532 | index += 1 533 | continue 534 | 535 | return available_cameras 536 | 537 | def get_camera_id(self) -> int | None: 538 | try: 539 | id = self.ui.combo_camera.currentText() 540 | return int(id) 541 | except : 542 | return None 543 | 544 | def on_camera_update(self): 545 | last = self.get_camera_id() 546 | 547 | cameras = self.list_available_cameras() 548 | self.ui.combo_camera.clear() 549 | self.ui.combo_camera.addItems(cameras) 550 | if last and str(last) in cameras: 551 | self.ui.combo_type.setCurrentText(str(last)) 552 | 553 | def on_format_change(self): 554 | self.format = self.ui.combo_format.currentText() 555 | 556 | if self.format != 'Specter': 557 | self.ui.combo_type.show() 558 | self.on_data_type_change() 559 | 560 | else: 561 | self.ui.combo_type.hide() 562 | self.data_type = None 563 | 564 | def on_data_type_change(self): 565 | if self.format == 'UR': 566 | self.data_type = self.ui.combo_type.currentText() 567 | 568 | def on_qr_display(self, frame): 569 | if frame is None: 570 | frame = QPixmap(self.ui.video_in.size()) 571 | frame.fill(QColor(FILL_COLOR)) 572 | 573 | self.ui.video_out.setPixmap(frame) 574 | 575 | def on_qr_read(self): 576 | if not self.read_qr.isRunning(): 577 | self.read_qr.end = False 578 | self.ui.data_in.setPlainText('') 579 | self.read_qr.start() 580 | else: 581 | self.read_qr.end = True 582 | 583 | def on_qr_data_read(self, data): 584 | self.ui.data_in.setWordWrapMode(QTextOption.WrapAnywhere) 585 | self.ui.data_in.setPlainText(data) 586 | 587 | def upd_camera_stream(self, frame): 588 | if frame is None: 589 | frame = QPixmap(self.ui.video_in.size()) 590 | frame.fill(QColor(FILL_COLOR)) 591 | 592 | self.ui.video_in.setPixmap(frame) 593 | 594 | def on_slider_move(self): 595 | self.ui.split_size.setText(f"Split size: {self.ui.send_slider.value()}") 596 | 597 | def on_btn_generate(self): 598 | data: str = self.ui.data_out.toPlainText() 599 | data.replace(' ', '').replace('\n', '') 600 | if not self.display_qr.isRunning() and data != '': 601 | 602 | if self.ui.no_split.isChecked(): 603 | _max = None 604 | else: 605 | _max = self.ui.send_slider.value() 606 | 607 | # print(f"max={_max}") 608 | qr = MultiQRCode.from_string(data, max=_max, type=self.data_type, format=self.format) 609 | if not qr: 610 | print("error creating MultiQRCode") 611 | return 612 | self.display_qr.qr_data = qr 613 | self.display_qr.start() 614 | 615 | self.ui.btn_generate.setText('Stop') 616 | 617 | else: 618 | self.stop_display.emit() 619 | self.ui.btn_generate.setText('Generate') 620 | 621 | def on_btn_clear(self): 622 | self.ui.data_out.setPlainText('') 623 | 624 | def select_data_type(self, data_type): 625 | self.data_type = data_type 626 | self.ui.combo_type.setCurrentText(data_type) 627 | 628 | def radio_select(self): 629 | if self.ui.desc_1.isChecked(): 630 | self.radio_selected = 'desc_1' 631 | self.select_data_type('Descriptor') 632 | 633 | elif self.ui.desc_2.isChecked(): 634 | self.radio_selected = 'desc_2' 635 | self.select_data_type('Descriptor') 636 | 637 | elif self.ui.desc_3.isChecked(): 638 | self.radio_selected = 'desc_3' 639 | self.select_data_type('Descriptor') 640 | 641 | elif self.ui.psbt_1.isChecked(): 642 | self.radio_selected = 'psbt_1' 643 | self.select_data_type('PSBT') 644 | 645 | elif self.ui.psbt_2.isChecked(): 646 | self.radio_selected = 'psbt_2' 647 | self.select_data_type('PSBT') 648 | 649 | elif self.ui.psbt_3.isChecked(): 650 | self.radio_selected = 'psbt_3' 651 | self.select_data_type('PSBT') 652 | 653 | elif self.ui.psbt_4.isChecked(): 654 | self.radio_selected = 'psbt_4' 655 | self.select_data_type('PSBT') 656 | 657 | elif self.ui.psbt_5.isChecked(): 658 | self.radio_selected = 'psbt_5' 659 | self.select_data_type('PSBT') 660 | 661 | elif self.ui.key_1.isChecked(): 662 | self.radio_selected = 'key_1' 663 | self.select_data_type('Key') 664 | 665 | elif self.ui.key_2.isChecked(): 666 | self.radio_selected = 'key_2' 667 | self.select_data_type('Key') 668 | 669 | elif self.ui.key_3.isChecked(): 670 | self.radio_selected = 'key_3' 671 | self.select_data_type('Key') 672 | 673 | elif self.ui.key_4.isChecked(): 674 | self.radio_selected = 'key_4' 675 | self.select_data_type('Key') 676 | 677 | elif self.ui.key_5.isChecked(): 678 | self.radio_selected = 'key_5' 679 | self.select_data_type('Key') 680 | 681 | else: 682 | return 683 | 684 | def on_radio_toggled(self): 685 | 686 | self.radio_select() 687 | self.load_config() 688 | 689 | if self.radio_selected in self.config.keys(): 690 | self.ui.data_out.setPlainText(self.config[self.radio_selected]) 691 | else: 692 | self.ui.data_out.setPlainText('') 693 | 694 | def on_btn_save(self): 695 | 696 | self.load_config() 697 | self.config[self.radio_selected] = self.ui.data_out.toPlainText() 698 | self.dump_config() 699 | 700 | 701 | if __name__ == '__main__': 702 | # the QUiLoader object needs to be initialized BEFORE the QApplication - https://stackoverflow.com/a/78041695 703 | loader = QUiLoader() 704 | app = QApplication(sys.argv) 705 | 706 | app.setStyle("Fusion") 707 | 708 | # Now use a palette to switch to dark colors: 709 | palette = QPalette() 710 | palette.setColor(QPalette.Window, QColor(53, 53, 53)) 711 | palette.setColor(QPalette.WindowText, Qt.white) 712 | palette.setColor(QPalette.Base, QColor(25, 25, 25)) 713 | palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) 714 | palette.setColor(QPalette.ToolTipBase, Qt.black) 715 | palette.setColor(QPalette.ToolTipText, Qt.white) 716 | palette.setColor(QPalette.Text, Qt.white) 717 | palette.setColor(QPalette.Button, QColor(53, 53, 53)) 718 | palette.setColor(QPalette.ButtonText, Qt.white) 719 | palette.setColor(QPalette.BrightText, Qt.red) 720 | palette.setColor(QPalette.Link, QColor(42, 130, 218)) 721 | palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) 722 | palette.setColor(QPalette.HighlightedText, Qt.black) 723 | app.setPalette(palette) 724 | 725 | main_win = MainWindow(loader) 726 | main_win.show() 727 | app.exec() 728 | 729 | --------------------------------------------------------------------------------