├── .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 |
--------------------------------------------------------------------------------