|
@ -15,22 +15,42 @@ import qrcode |
|
|
import traceback |
|
|
import traceback |
|
|
from hashlib import sha256 |
|
|
from hashlib import sha256 |
|
|
from decimal import Decimal |
|
|
from decimal import Decimal |
|
|
import binascii |
|
|
from typing import NamedTuple, Optional, Dict, Tuple |
|
|
|
|
|
|
|
|
from PyQt5.QtPrintSupport import QPrinter |
|
|
from PyQt5.QtPrintSupport import QPrinter |
|
|
|
|
|
|
|
|
from electrum.plugin import BasePlugin, hook |
|
|
from electrum.plugin import BasePlugin, hook |
|
|
from electrum.i18n import _ |
|
|
from electrum.i18n import _ |
|
|
from electrum.util import to_bytes, make_dir, InvalidPassword, UserCancelled |
|
|
from electrum.util import to_bytes, make_dir, InvalidPassword, UserCancelled, bh2u, bfh |
|
|
from electrum.gui.qt.util import * |
|
|
from electrum.gui.qt.util import * |
|
|
from electrum.gui.qt.qrtextedit import ScanQRTextEdit |
|
|
from electrum.gui.qt.qrtextedit import ScanQRTextEdit |
|
|
from electrum.gui.qt.main_window import StatusBarButton |
|
|
from electrum.gui.qt.main_window import StatusBarButton |
|
|
|
|
|
|
|
|
from .hmac_drbg import DRBG |
|
|
from .hmac_drbg import DRBG |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VersionedSeed(NamedTuple): |
|
|
|
|
|
version: str |
|
|
|
|
|
seed: str |
|
|
|
|
|
checksum: str |
|
|
|
|
|
|
|
|
|
|
|
def get_ui_string_version_plus_seed(self): |
|
|
|
|
|
version, seed = self.version, self.seed |
|
|
|
|
|
assert isinstance(version, str) and len(version) == 1, version |
|
|
|
|
|
assert isinstance(seed, str) and len(seed) >= 32 |
|
|
|
|
|
ret = version + seed |
|
|
|
|
|
ret = ret.upper() |
|
|
|
|
|
return ' '.join(ret[i : i+4] for i in range(0, len(ret), 4)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Plugin(BasePlugin): |
|
|
class Plugin(BasePlugin): |
|
|
|
|
|
|
|
|
|
|
|
LATEST_VERSION = '1' |
|
|
|
|
|
KNOWN_VERSIONS = ('0', '1') |
|
|
|
|
|
assert LATEST_VERSION in KNOWN_VERSIONS |
|
|
|
|
|
|
|
|
MAX_PLAINTEXT_LEN = 189 # chars |
|
|
MAX_PLAINTEXT_LEN = 189 # chars |
|
|
|
|
|
SIZE = (159, 97) |
|
|
|
|
|
|
|
|
def __init__(self, parent, config, name): |
|
|
def __init__(self, parent, config, name): |
|
|
BasePlugin.__init__(self, parent, config, name) |
|
|
BasePlugin.__init__(self, parent, config, name) |
|
@ -44,8 +64,6 @@ class Plugin(BasePlugin): |
|
|
self.calibration_h = self.config.get('calibration_h') |
|
|
self.calibration_h = self.config.get('calibration_h') |
|
|
self.calibration_v = self.config.get('calibration_v') |
|
|
self.calibration_v = self.config.get('calibration_v') |
|
|
|
|
|
|
|
|
self.version = '1' |
|
|
|
|
|
self.size = (159, 97) |
|
|
|
|
|
self.f_size = QSize(1014*2, 642*2) |
|
|
self.f_size = QSize(1014*2, 642*2) |
|
|
self.abstand_h = 21 |
|
|
self.abstand_h = 21 |
|
|
self.abstand_v = 34 |
|
|
self.abstand_v = 34 |
|
@ -90,7 +108,6 @@ class Plugin(BasePlugin): |
|
|
self.wallet = window.parent().wallet |
|
|
self.wallet = window.parent().wallet |
|
|
self.update_wallet_name(self.wallet) |
|
|
self.update_wallet_name(self.wallet) |
|
|
self.user_input = False |
|
|
self.user_input = False |
|
|
self.noise_seed = False |
|
|
|
|
|
|
|
|
|
|
|
self.d = WindowModalDialog(window, "Setup Dialog") |
|
|
self.d = WindowModalDialog(window, "Setup Dialog") |
|
|
self.d.setMinimumWidth(500) |
|
|
self.d.setMinimumWidth(500) |
|
@ -145,39 +162,36 @@ class Plugin(BasePlugin): |
|
|
return ''.join(text.split()).lower() |
|
|
return ''.join(text.split()).lower() |
|
|
|
|
|
|
|
|
def on_edit(self): |
|
|
def on_edit(self): |
|
|
s = self.get_noise() |
|
|
txt = self.get_noise() |
|
|
b = self.is_noise(s) |
|
|
versioned_seed = self.get_versioned_seed_from_user_input(txt) |
|
|
if b: |
|
|
if versioned_seed: |
|
|
self.noise_seed = s[1:-3] |
|
|
self.versioned_seed = versioned_seed |
|
|
self.user_input = True |
|
|
self.user_input = bool(versioned_seed) |
|
|
self.next_button.setEnabled(b) |
|
|
self.next_button.setEnabled(bool(versioned_seed)) |
|
|
|
|
|
|
|
|
def code_hashid(self, txt): |
|
|
@classmethod |
|
|
|
|
|
def code_hashid(cls, txt: str) -> str: |
|
|
x = to_bytes(txt, 'utf8') |
|
|
x = to_bytes(txt, 'utf8') |
|
|
hash = sha256(x).hexdigest() |
|
|
hash = sha256(x).hexdigest() |
|
|
return hash[-3:].upper() |
|
|
return hash[-3:].upper() |
|
|
|
|
|
|
|
|
def is_noise(self, txt): |
|
|
@classmethod |
|
|
if (len(txt) >= 34): |
|
|
def get_versioned_seed_from_user_input(cls, txt: str) -> Optional[VersionedSeed]: |
|
|
try: |
|
|
if len(txt) < 34: |
|
|
int(txt, 16) |
|
|
return None |
|
|
except: |
|
|
try: |
|
|
self.user_input = False |
|
|
int(txt, 16) |
|
|
return False |
|
|
except: |
|
|
else: |
|
|
return None |
|
|
id = self.code_hashid(txt[:-3]) |
|
|
version = txt[0] |
|
|
if (txt[-3:].upper() == id.upper()): |
|
|
if version not in cls.KNOWN_VERSIONS: |
|
|
self.code_id = id |
|
|
return None |
|
|
self.user_input = True |
|
|
checksum = cls.code_hashid(txt[:-3]) |
|
|
return True |
|
|
if txt[-3:].upper() != checksum.upper(): |
|
|
else: |
|
|
return None |
|
|
return False |
|
|
return VersionedSeed(version=version.upper(), |
|
|
else: |
|
|
seed=txt[1:-3].upper(), |
|
|
|
|
|
checksum=checksum.upper()) |
|
|
if (len(txt)>0 and txt[0]=='0'): |
|
|
|
|
|
self.d.show_message(''.join(["<b>",_("Warning: "), "</b>", _("Revealers starting with 0 had a vulnerability and are not supported.")])) |
|
|
|
|
|
self.user_input = False |
|
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
|
|
def make_digital(self, dialog): |
|
|
def make_digital(self, dialog): |
|
|
self.make_rawnoise(True) |
|
|
self.make_rawnoise(True) |
|
@ -185,7 +199,9 @@ class Plugin(BasePlugin): |
|
|
self.d.close() |
|
|
self.d.close() |
|
|
|
|
|
|
|
|
def get_path_to_revealer_file(self, ext: str= '') -> str: |
|
|
def get_path_to_revealer_file(self, ext: str= '') -> str: |
|
|
filename = self.filename_prefix + self.version + "_" + self.code_id + ext |
|
|
version = self.versioned_seed.version |
|
|
|
|
|
code_id = self.versioned_seed.checksum |
|
|
|
|
|
filename = self.filename_prefix + version + "_" + code_id + ext |
|
|
path = os.path.join(self.base_dir, filename) |
|
|
path = os.path.join(self.base_dir, filename) |
|
|
return os.path.normcase(os.path.abspath(path)) |
|
|
return os.path.normcase(os.path.abspath(path)) |
|
|
|
|
|
|
|
@ -195,7 +211,9 @@ class Plugin(BasePlugin): |
|
|
|
|
|
|
|
|
def bcrypt(self, dialog): |
|
|
def bcrypt(self, dialog): |
|
|
self.rawnoise = False |
|
|
self.rawnoise = False |
|
|
dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at: ").format(self.was, self.version, self.code_id), |
|
|
version = self.versioned_seed.version |
|
|
|
|
|
code_id = self.versioned_seed.checksum |
|
|
|
|
|
dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at: ").format(self.was, version, code_id), |
|
|
"<b>", self.get_path_to_revealer_file(), "</b>", "<br/>", |
|
|
"<b>", self.get_path_to_revealer_file(), "</b>", "<br/>", |
|
|
"<br/>", "<b>", _("Always check you backups.")])) |
|
|
"<br/>", "<b>", _("Always check you backups.")])) |
|
|
dialog.close() |
|
|
dialog.close() |
|
@ -206,7 +224,9 @@ class Plugin(BasePlugin): |
|
|
dialog.close() |
|
|
dialog.close() |
|
|
|
|
|
|
|
|
def bdone(self, dialog): |
|
|
def bdone(self, dialog): |
|
|
dialog.show_message(''.join([_("Digital Revealer ({}_{}) saved as PNG and PDF at:").format(self.version, self.code_id), |
|
|
version = self.versioned_seed.version |
|
|
|
|
|
code_id = self.versioned_seed.checksum |
|
|
|
|
|
dialog.show_message(''.join([_("Digital Revealer ({}_{}) saved as PNG and PDF at:").format(version, code_id), |
|
|
"<br/>","<b>", self.get_path_to_revealer_file(), '</b>'])) |
|
|
"<br/>","<b>", self.get_path_to_revealer_file(), '</b>'])) |
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -241,7 +261,8 @@ class Plugin(BasePlugin): |
|
|
logo.setAlignment(Qt.AlignLeft) |
|
|
logo.setAlignment(Qt.AlignLeft) |
|
|
hbox.addSpacing(16) |
|
|
hbox.addSpacing(16) |
|
|
self.vbox.addWidget(WWLabel("<b>" + _("Revealer Secret Backup Plugin") + "</b><br>" |
|
|
self.vbox.addWidget(WWLabel("<b>" + _("Revealer Secret Backup Plugin") + "</b><br>" |
|
|
+ _("Ready to encrypt for revealer {}").format(self.version+'_'+self.code_id ))) |
|
|
+ _("Ready to encrypt for revealer {}") |
|
|
|
|
|
.format(self.versioned_seed.version+'_'+self.versioned_seed.checksum))) |
|
|
self.vbox.addSpacing(11) |
|
|
self.vbox.addSpacing(11) |
|
|
hbox.addLayout(self.vbox) |
|
|
hbox.addLayout(self.vbox) |
|
|
grid = QGridLayout() |
|
|
grid = QGridLayout() |
|
@ -294,7 +315,7 @@ class Plugin(BasePlugin): |
|
|
else: |
|
|
else: |
|
|
txt = self.txt.upper() |
|
|
txt = self.txt.upper() |
|
|
|
|
|
|
|
|
img = QImage(self.size[0],self.size[1], QImage.Format_Mono) |
|
|
img = QImage(self.SIZE[0], self.SIZE[1], QImage.Format_Mono) |
|
|
bitmap = QBitmap.fromImage(img, Qt.MonoOnly) |
|
|
bitmap = QBitmap.fromImage(img, Qt.MonoOnly) |
|
|
bitmap.fill(Qt.white) |
|
|
bitmap.fill(Qt.white) |
|
|
painter = QPainter() |
|
|
painter = QPainter() |
|
@ -325,7 +346,7 @@ class Plugin(BasePlugin): |
|
|
while len(' '.join(map(str, temp_seed))) > max_letters: |
|
|
while len(' '.join(map(str, temp_seed))) > max_letters: |
|
|
nwords = nwords - 1 |
|
|
nwords = nwords - 1 |
|
|
temp_seed = seed_array[:nwords] |
|
|
temp_seed = seed_array[:nwords] |
|
|
painter.drawText(QRect(0, linespace*n , self.size[0], self.size[1]), Qt.AlignHCenter, ' '.join(map(str, temp_seed))) |
|
|
painter.drawText(QRect(0, linespace*n , self.SIZE[0], self.SIZE[1]), Qt.AlignHCenter, ' '.join(map(str, temp_seed))) |
|
|
del seed_array[:nwords] |
|
|
del seed_array[:nwords] |
|
|
|
|
|
|
|
|
painter.end() |
|
|
painter.end() |
|
@ -337,43 +358,55 @@ class Plugin(BasePlugin): |
|
|
return img |
|
|
return img |
|
|
|
|
|
|
|
|
def make_rawnoise(self, create_revealer=False): |
|
|
def make_rawnoise(self, create_revealer=False): |
|
|
w = self.size[0] |
|
|
if not self.user_input: |
|
|
h = self.size[1] |
|
|
version = self.LATEST_VERSION |
|
|
|
|
|
hex_seed = bh2u(os.urandom(16)) |
|
|
|
|
|
checksum = self.code_hashid(version + hex_seed) |
|
|
|
|
|
self.versioned_seed = VersionedSeed(version=version.upper(), |
|
|
|
|
|
seed=hex_seed.upper(), |
|
|
|
|
|
checksum=checksum.upper()) |
|
|
|
|
|
assert self.versioned_seed |
|
|
|
|
|
w, h = self.SIZE |
|
|
rawnoise = QImage(w, h, QImage.Format_Mono) |
|
|
rawnoise = QImage(w, h, QImage.Format_Mono) |
|
|
|
|
|
|
|
|
if(self.noise_seed == False): |
|
|
noise_map = self.get_noise_map(self.versioned_seed) |
|
|
self.noise_seed = random.SystemRandom().getrandbits(128) |
|
|
for (x,y), pixel in noise_map.items(): |
|
|
self.hex_noise = format(self.noise_seed, '032x') |
|
|
rawnoise.setPixel(x, y, pixel) |
|
|
self.hex_noise = self.version + str(self.hex_noise) |
|
|
|
|
|
|
|
|
|
|
|
if (self.user_input == True): |
|
|
|
|
|
self.noise_seed = int(self.noise_seed, 16) |
|
|
|
|
|
self.hex_noise = self.version + str(format(self.noise_seed, '032x')) |
|
|
|
|
|
|
|
|
|
|
|
self.code_id = self.code_hashid(self.hex_noise) |
|
|
|
|
|
self.hex_noise = ' '.join(self.hex_noise[i:i+4] for i in range(0,len(self.hex_noise),4)) |
|
|
|
|
|
|
|
|
|
|
|
entropy = binascii.unhexlify(str(format(self.noise_seed, '032x'))) |
|
|
|
|
|
code_id = binascii.unhexlify(self.version + self.code_id) |
|
|
|
|
|
|
|
|
|
|
|
drbg = DRBG(entropy + code_id) |
|
|
|
|
|
noise_array=bin(int.from_bytes(drbg.generate(1929), 'big'))[2:] |
|
|
|
|
|
|
|
|
|
|
|
i=0 |
|
|
|
|
|
for x in range(w): |
|
|
|
|
|
for y in range(h): |
|
|
|
|
|
rawnoise.setPixel(x,y,int(noise_array[i])) |
|
|
|
|
|
i+=1 |
|
|
|
|
|
|
|
|
|
|
|
self.rawnoise = rawnoise |
|
|
self.rawnoise = rawnoise |
|
|
if create_revealer==True: |
|
|
if create_revealer: |
|
|
self.make_revealer() |
|
|
self.make_revealer() |
|
|
self.noise_seed = False |
|
|
|
|
|
|
|
|
@classmethod |
|
|
|
|
|
def get_noise_map(self, versioned_seed: VersionedSeed) -> Dict[Tuple[int, int], int]: |
|
|
|
|
|
"""Returns a map from (x,y) coordinate to pixel value 0/1, to be used as noise.""" |
|
|
|
|
|
w, h = self.SIZE |
|
|
|
|
|
version = versioned_seed.version |
|
|
|
|
|
hex_seed = versioned_seed.seed |
|
|
|
|
|
checksum = versioned_seed.checksum |
|
|
|
|
|
noise_map = {} |
|
|
|
|
|
if version == '0': |
|
|
|
|
|
random.seed(int(hex_seed, 16)) |
|
|
|
|
|
for x in range(w): |
|
|
|
|
|
for y in range(h): |
|
|
|
|
|
noise_map[(x, y)] = random.randint(0, 1) |
|
|
|
|
|
elif version == '1': |
|
|
|
|
|
prng_seed = bfh(hex_seed + version + checksum) |
|
|
|
|
|
drbg = DRBG(prng_seed) |
|
|
|
|
|
num_noise_bytes = 1929 # ~ w*h |
|
|
|
|
|
noise_array = bin(int.from_bytes(drbg.generate(num_noise_bytes), 'big'))[2:] |
|
|
|
|
|
i = 0 |
|
|
|
|
|
for x in range(w): |
|
|
|
|
|
for y in range(h): |
|
|
|
|
|
noise_map[(x, y)] = int(noise_array[i]) |
|
|
|
|
|
i += 1 |
|
|
|
|
|
else: |
|
|
|
|
|
raise Exception(f"unexpected revealer version: {version}") |
|
|
|
|
|
return noise_map |
|
|
|
|
|
|
|
|
def make_calnoise(self): |
|
|
def make_calnoise(self): |
|
|
random.seed(self.calibration_noise) |
|
|
random.seed(self.calibration_noise) |
|
|
w = self.size[0] |
|
|
w, h = self.SIZE |
|
|
h = self.size[1] |
|
|
|
|
|
rawnoise = QImage(w, h, QImage.Format_Mono) |
|
|
rawnoise = QImage(w, h, QImage.Format_Mono) |
|
|
for x in range(w): |
|
|
for x in range(w): |
|
|
for y in range(h): |
|
|
for y in range(h): |
|
@ -422,7 +455,7 @@ class Plugin(BasePlugin): |
|
|
return cypherseed |
|
|
return cypherseed |
|
|
|
|
|
|
|
|
def calibration(self): |
|
|
def calibration(self): |
|
|
img = QImage(self.size[0],self.size[1], QImage.Format_Mono) |
|
|
img = QImage(self.SIZE[0], self.SIZE[1], QImage.Format_Mono) |
|
|
bitmap = QBitmap.fromImage(img, Qt.MonoOnly) |
|
|
bitmap = QBitmap.fromImage(img, Qt.MonoOnly) |
|
|
bitmap.fill(Qt.black) |
|
|
bitmap.fill(Qt.black) |
|
|
self.make_calnoise() |
|
|
self.make_calnoise() |
|
@ -586,7 +619,8 @@ class Plugin(BasePlugin): |
|
|
(base_img.height()-((total_distance_h)))-((border_thick*8)/2)-(border_thick/2)-2) |
|
|
(base_img.height()-((total_distance_h)))-((border_thick*8)/2)-(border_thick/2)-2) |
|
|
painter.setPen(QColor(0,0,0,255)) |
|
|
painter.setPen(QColor(0,0,0,255)) |
|
|
painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick - 11, |
|
|
painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick - 11, |
|
|
base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.version + '_'+self.code_id) |
|
|
base_img.height()-total_distance_h - border_thick), Qt.AlignRight, |
|
|
|
|
|
self.versioned_seed.version + '_'+self.versioned_seed.checksum) |
|
|
painter.end() |
|
|
painter.end() |
|
|
|
|
|
|
|
|
else: # revealer |
|
|
else: # revealer |
|
@ -635,12 +669,13 @@ class Plugin(BasePlugin): |
|
|
painter.setPen(QColor(0,0,0,255)) |
|
|
painter.setPen(QColor(0,0,0,255)) |
|
|
painter.drawText(QRect(((base_img.width()/2) +21)-qr_size, base_img.height()-107, |
|
|
painter.drawText(QRect(((base_img.width()/2) +21)-qr_size, base_img.height()-107, |
|
|
base_img.width()-total_distance_h - border_thick -93, |
|
|
base_img.width()-total_distance_h - border_thick -93, |
|
|
base_img.height()-total_distance_h - border_thick), Qt.AlignLeft, self.hex_noise.upper()) |
|
|
base_img.height()-total_distance_h - border_thick), Qt.AlignLeft, self.versioned_seed.get_ui_string_version_plus_seed()) |
|
|
painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick -3 -qr_size, |
|
|
painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick -3 -qr_size, |
|
|
base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.code_id) |
|
|
base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.versioned_seed.checksum) |
|
|
|
|
|
|
|
|
# draw qr code |
|
|
# draw qr code |
|
|
qr_qt = self.paintQR(self.hex_noise.upper() +self.code_id) |
|
|
qr_qt = self.paintQR(self.versioned_seed.get_ui_string_version_plus_seed() |
|
|
|
|
|
+ self.versioned_seed.checksum) |
|
|
target = QRectF(base_img.width()-65-qr_size, |
|
|
target = QRectF(base_img.width()-65-qr_size, |
|
|
base_img.height()-65-qr_size, |
|
|
base_img.height()-65-qr_size, |
|
|
qr_size, qr_size ) |
|
|
qr_size, qr_size ) |
|
|