Merge pull request #496 from trezor/tsusanka/pre-slip39

Store mnemonic as bytes
pull/25/head
Tomas Susanka 5 years ago committed by GitHub
commit 12dd548938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,7 +2,7 @@ from trezor import wire
from trezor.crypto import bip32
from apps.cardano import SEED_NAMESPACE
from apps.common import cache, storage
from apps.common import cache, mnemonic, storage
from apps.common.request_passphrase import protect_by_passphrase
@ -33,7 +33,7 @@ async def get_keychain(ctx: wire.Context) -> Keychain:
if passphrase is None:
passphrase = await protect_by_passphrase(ctx)
cache.set_passphrase(passphrase)
root = bip32.from_mnemonic_cardano(storage.get_mnemonic(), passphrase)
root = bip32.from_mnemonic_cardano(mnemonic.restore(), passphrase)
# derive the namespaced root node
for i in SEED_NAMESPACE[0]:

@ -924,6 +924,46 @@ COINS = [
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Qtum",
coin_shortcut="QTUM",
address_type=58,
address_type_p2sh=50,
maxfee_kb=40000000,
signed_message_header="Qtum Signed Message:\n",
xpub_magic=0x0488b21e,
xpub_magic_segwit_p2sh=0x049d7cb2,
xpub_magic_segwit_native=0x04b24746,
bech32_prefix="qc",
cashaddr_prefix=None,
slip44=2301,
segwit=True,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Qtum Testnet",
coin_shortcut="tQTUM",
address_type=120,
address_type_p2sh=110,
maxfee_kb=40000000,
signed_message_header="Qtum Signed Message:\n",
xpub_magic=0x043587cf,
xpub_magic_segwit_p2sh=0x044a5262,
xpub_magic_segwit_native=0x045f1cf6,
bech32_prefix="tq",
cashaddr_prefix=None,
slip44=1,
segwit=True,
fork_id=None,
force_bip143=False,
bip115=False,
decred=False,
curve_name='secp256k1',
),
CoinInfo(
coin_name="Ravencoin",
coin_shortcut="RVN",

@ -0,0 +1,46 @@
from trezor import ui
from trezor.crypto import bip39
from apps.common import storage
TYPE_BIP39 = 0
def get() -> (bytes, int):
mnemonic_secret = storage.get_mnemonic_secret()
mnemonic_type = storage.get_mnemonic_type()
return mnemonic_secret, mnemonic_type
def get_seed(passphrase: str = ""):
secret, mnemonic_type = get()
_start_progress()
if mnemonic_type == TYPE_BIP39:
return bip39.seed(secret.decode(), passphrase, _render_progress)
def process(mnemonics: list, mnemonic_type: int):
if mnemonic_type == TYPE_BIP39:
return mnemonics[0].encode()
else:
raise RuntimeError("Unknown mnemonic type")
def restore() -> str:
secret, mnemonic_type = get()
if mnemonic_type == TYPE_BIP39:
return secret.decode()
def _start_progress():
ui.backlight_slide_sync(ui.BACKLIGHT_DIM)
ui.display.clear()
ui.header("Please wait")
ui.display.refresh()
ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL)
def _render_progress(progress: int, total: int):
p = int(1000 * progress / total)
ui.display.loader(p, 18, ui.WHITE, ui.BG)
ui.display.refresh()

@ -1,7 +1,7 @@
from trezor import ui, wire
from trezor.crypto import bip32, bip39
from trezor.crypto import bip32
from apps.common import cache, storage
from apps.common import cache, mnemonic, storage
from apps.common.request_passphrase import protect_by_passphrase
allow = list
@ -66,32 +66,17 @@ async def _compute_seed(ctx: wire.Context) -> bytes:
if passphrase is None:
passphrase = await protect_by_passphrase(ctx)
cache.set_passphrase(passphrase)
_start_bip39_progress()
seed = bip39.seed(storage.get_mnemonic(), passphrase, _render_bip39_progress)
seed = mnemonic.get_seed(passphrase)
cache.set_seed(seed)
return seed
def _start_bip39_progress():
ui.backlight_slide_sync(ui.BACKLIGHT_DIM)
ui.display.clear()
ui.header("Please wait")
ui.display.refresh()
ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL)
def _render_bip39_progress(progress: int, total: int):
p = int(1000 * progress / total)
ui.display.loader(p, 18, ui.WHITE, ui.BG)
ui.display.refresh()
def derive_node_without_passphrase(
path: list, curve_name: str = "secp256k1"
) -> bip32.HDNode:
if not storage.is_initialized():
raise Exception("Device is not initialized")
seed = bip39.seed(storage.get_mnemonic(), "")
seed = mnemonic.get_seed()
node = bip32.from_seed(seed, curve_name)
node.derive_path(path)
return node

@ -18,7 +18,7 @@ _COUNTER_TAIL_LEN = 8
_APP = const(0x01) # app namespace
_DEVICE_ID = const(0x00) # bytes
_VERSION = const(0x01) # int
_MNEMONIC = const(0x02) # str
_MNEMONIC_SECRET = const(0x02) # bytes
_LANGUAGE = const(0x03) # str
_LABEL = const(0x04) # str
_USE_PASSPHRASE = const(0x05) # bool (0x01 or empty)
@ -30,6 +30,7 @@ _PASSPHRASE_SOURCE = const(0x0A) # int
_UNFINISHED_BACKUP = const(0x0B) # bool (0x01 or empty)
_AUTOLOCK_DELAY_MS = const(0x0C) # int
_NO_BACKUP = const(0x0D) # bool (0x01 or empty)
_MNEMONIC_TYPE = const(0x0E) # int
# fmt: on
@ -44,6 +45,17 @@ def _get_bool(app: int, key: int, public: bool = False) -> bool:
return config.get(app, key, public) == _TRUE_BYTE
def _set_uint8(app: int, key: int, val: int):
config.set(app, key, val.to_bytes(1, "big"))
def _get_uint8(app: int, key: int) -> int:
val = config.get(app, key)
if not val:
return None
return int.from_bytes(val, "big")
def _new_device_id() -> str:
return hexlify(random.bytes(12)).decode().upper()
@ -67,11 +79,15 @@ def get_label() -> str:
return label.decode()
def get_mnemonic() -> str:
mnemonic = config.get(_APP, _MNEMONIC)
def get_mnemonic_secret() -> bytes:
mnemonic = config.get(_APP, _MNEMONIC_SECRET)
if mnemonic is None:
return None
return mnemonic.decode()
return mnemonic
def get_mnemonic_type() -> int:
return _get_uint8(_APP, _MNEMONIC_TYPE)
def has_passphrase() -> bool:
@ -82,8 +98,11 @@ def get_homescreen() -> bytes:
return config.get(_APP, _HOMESCREEN, True) # public
def load_mnemonic(mnemonic: str, needs_backup: bool, no_backup: bool) -> None:
config.set(_APP, _MNEMONIC, mnemonic.encode())
def store_mnemonic(
secret: bytes, mnemonic_type: int, needs_backup: bool, no_backup: bool
) -> None:
config.set(_APP, _MNEMONIC_SECRET, secret)
_set_uint8(_APP, _MNEMONIC_TYPE, mnemonic_type)
config.set(_APP, _VERSION, _STORAGE_VERSION)
_set_bool(_APP, _NO_BACKUP, no_backup)
if not no_backup:

@ -9,7 +9,7 @@ if __debug__:
from trezor.messages.DebugLinkState import DebugLinkState
from trezor.ui import confirm, swipe
from trezor.wire import register, protobuf_workflow
from apps.common import storage
from apps.common import storage, mnemonic
reset_internal_entropy = None
reset_current_words = None
@ -29,7 +29,7 @@ if __debug__:
async def dispatch_DebugLinkGetState(ctx, msg):
m = DebugLinkState()
m.mnemonic = storage.get_mnemonic()
m.mnemonic_secret, m.mnemonic_type = mnemonic.get()
m.passphrase_protection = storage.has_passphrase()
m.reset_word_pos = reset_word_index
m.reset_entropy = reset_internal_entropy

@ -142,6 +142,13 @@ NETWORKS = [
name="Mix",
rskip60=False,
),
NetworkInfo(
chain_id=237,
slip44=237,
shortcut="DXN",
name="DEXON",
rskip60=False,
),
NetworkInfo(
chain_id=820,
slip44=820,

@ -1,7 +1,7 @@
from trezor import wire
from trezor.messages.Success import Success
from apps.common import storage
from apps.common import mnemonic, storage
from apps.management.reset_device import (
check_mnemonic,
show_mnemonic,
@ -16,7 +16,7 @@ async def backup_device(ctx, msg):
if not storage.needs_backup():
raise wire.ProcessError("Seed already backed up")
mnemonic = storage.get_mnemonic()
words = mnemonic.restore()
# warn user about mnemonic safety
await show_warning(ctx)
@ -26,8 +26,8 @@ async def backup_device(ctx, msg):
while True:
# show mnemonic and require confirmation of a random word
await show_mnemonic(ctx, mnemonic)
if await check_mnemonic(ctx, mnemonic):
await show_mnemonic(ctx, words)
if await check_mnemonic(ctx, words):
break
await show_wrong_entry(ctx)

@ -4,7 +4,7 @@ from trezor.messages.Success import Success
from trezor.pin import pin_to_int
from trezor.ui.text import Text
from apps.common import storage
from apps.common import mnemonic, storage
from apps.common.confirm import require_confirm
@ -24,7 +24,13 @@ async def load_device(ctx, msg):
text.normal("Continue only if you", "know what you are doing!")
await require_confirm(ctx, text)
storage.load_mnemonic(mnemonic=msg.mnemonic, needs_backup=True, no_backup=False)
secret = mnemonic.process([msg.mnemonic], mnemonic.TYPE_BIP39)
storage.store_mnemonic(
secret=secret,
mnemonic_type=mnemonic.TYPE_BIP39,
needs_backup=True,
no_backup=False,
)
storage.load_settings(use_passphrase=msg.passphrase_protection, label=msg.label)
if msg.pin:
config.change_pin(pin_to_int(""), pin_to_int(msg.pin))

@ -15,7 +15,7 @@ from trezor.ui.text import Text
from trezor.ui.word_select import WordSelector
from trezor.utils import consteq, format_ordinal
from apps.common import storage
from apps.common import mnemonic, storage
from apps.common.confirm import require_confirm
from apps.management.change_pin import request_pin_ack, request_pin_confirm
@ -47,11 +47,11 @@ async def recovery_device(ctx, msg):
wordcount = await request_wordcount(ctx)
# ask for mnemonic words one by one
mnemonic = await request_mnemonic(ctx, wordcount)
words = await request_mnemonic(ctx, wordcount)
# check mnemonic validity
if msg.enforce_wordlist or msg.dry_run:
if not bip39.check(mnemonic):
if not bip39.check(words):
raise wire.ProcessError("Mnemonic is not valid")
# ask for pin repeatedly
@ -60,10 +60,13 @@ async def recovery_device(ctx, msg):
else:
newpin = ""
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
# dry run
if msg.dry_run:
digest_input = sha256(mnemonic).digest()
digest_stored = sha256(storage.get_mnemonic()).digest()
digest_input = sha256(secret).digest()
stored, _ = mnemonic.get()
digest_stored = sha256(stored).digest()
if consteq(digest_stored, digest_input):
return Success(
message="The seed is valid and matches the one in the device"
@ -78,7 +81,12 @@ async def recovery_device(ctx, msg):
config.change_pin(pin_to_int(""), pin_to_int(newpin))
storage.set_u2f_counter(msg.u2f_counter)
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
storage.load_mnemonic(mnemonic=mnemonic, needs_backup=False, no_backup=False)
storage.store_mnemonic(
secret=secret,
mnemonic_type=mnemonic.TYPE_BIP39,
needs_backup=False,
no_backup=False,
)
return Success(message="Device recovered")

@ -14,7 +14,7 @@ from trezor.ui.scroll import Scrollpage, animate_swipe, paginate
from trezor.ui.text import Text
from trezor.utils import chunks, format_ordinal
from apps.common import storage
from apps.common import mnemonic, storage
from apps.common.confirm import require_confirm
from apps.management.change_pin import request_pin_confirm
@ -59,7 +59,7 @@ async def reset_device(ctx, msg):
# request external entropy and compute mnemonic
ent_ack = await ctx.call(EntropyRequest(), MessageType.EntropyAck)
mnemonic = generate_mnemonic(msg.strength, internal_ent, ent_ack.entropy)
words = generate_mnemonic(msg.strength, internal_ent, ent_ack.entropy)
if not msg.skip_backup and not msg.no_backup:
# require confirmation of the mnemonic safety
@ -67,8 +67,8 @@ async def reset_device(ctx, msg):
# show mnemonic and require confirmation of a random word
while True:
await show_mnemonic(ctx, mnemonic)
if await check_mnemonic(ctx, mnemonic):
await show_mnemonic(ctx, words)
if await check_mnemonic(ctx, words):
break
await show_wrong_entry(ctx)
@ -77,10 +77,14 @@ async def reset_device(ctx, msg):
if not config.change_pin(pin_to_int(""), pin_to_int(newpin)):
raise wire.ProcessError("Could not change PIN")
secret = mnemonic.process([words], mnemonic.TYPE_BIP39)
# write settings and mnemonic into storage
storage.load_settings(label=msg.label, use_passphrase=msg.passphrase_protection)
storage.load_mnemonic(
mnemonic=mnemonic, needs_backup=msg.skip_backup, no_backup=msg.no_backup
storage.store_mnemonic(
secret=secret,
mnemonic_type=mnemonic.TYPE_BIP39,
needs_backup=msg.skip_backup,
no_backup=msg.no_backup,
)
# show success message. if we skipped backup, it's possible that homescreen
@ -98,8 +102,7 @@ def generate_mnemonic(strength: int, int_entropy: bytes, ext_entropy: bytes) ->
ehash.update(int_entropy)
ehash.update(ext_entropy)
entropy = ehash.digest()
mnemonic = bip39.from_data(entropy[: strength // 8])
return mnemonic
return bip39.from_data(entropy[: strength // 8])
async def show_warning(ctx):

@ -0,0 +1,19 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
class BinanceAddress(p.MessageType):
MESSAGE_WIRE_TYPE = 701
def __init__(
self,
address: str = None,
) -> None:
self.address = address
@classmethod
def get_fields(cls):
return {
1: ('address', p.UnicodeType, 0),
}

@ -0,0 +1,25 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
class BinanceCancelMsg(p.MessageType):
MESSAGE_WIRE_TYPE = 708
def __init__(
self,
refid: str = None,
sender: str = None,
symbol: str = None,
) -> None:
self.refid = refid
self.sender = sender
self.symbol = symbol
@classmethod
def get_fields(cls):
return {
1: ('refid', p.UnicodeType, 0),
2: ('sender', p.UnicodeType, 0),
3: ('symbol', p.UnicodeType, 0),
}

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import List
except ImportError:
List = None # type: ignore
class BinanceGetAddress(p.MessageType):
MESSAGE_WIRE_TYPE = 700
def __init__(
self,
address_n: List[int] = None,
show_display: bool = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.show_display = show_display
@classmethod
def get_fields(cls):
return {
1: ('address_n', p.UVarintType, p.FLAG_REPEATED),
2: ('show_display', p.BoolType, 0),
}

@ -0,0 +1,28 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import List
except ImportError:
List = None # type: ignore
class BinanceGetPublicKey(p.MessageType):
MESSAGE_WIRE_TYPE = 702
def __init__(
self,
address_n: List[int] = None,
show_display: bool = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.show_display = show_display
@classmethod
def get_fields(cls):
return {
1: ('address_n', p.UVarintType, p.FLAG_REPEATED),
2: ('show_display', p.BoolType, 0),
}

@ -0,0 +1,40 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
class BinanceOrderMsg(p.MessageType):
MESSAGE_WIRE_TYPE = 707
def __init__(
self,
id: str = None,
ordertype: int = None,
price: int = None,
quantity: int = None,
sender: str = None,
side: int = None,
symbol: str = None,
timeinforce: int = None,
) -> None:
self.id = id
self.ordertype = ordertype
self.price = price
self.quantity = quantity
self.sender = sender
self.side = side
self.symbol = symbol
self.timeinforce = timeinforce
@classmethod
def get_fields(cls):
return {
1: ('id', p.UnicodeType, 0),
2: ('ordertype', p.UVarintType, 0),
3: ('price', p.SVarintType, 0),
4: ('quantity', p.SVarintType, 0),
5: ('sender', p.UnicodeType, 0),
6: ('side', p.UVarintType, 0),
7: ('symbol', p.UnicodeType, 0),
8: ('timeinforce', p.UVarintType, 0),
}

@ -0,0 +1,19 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
class BinancePublicKey(p.MessageType):
MESSAGE_WIRE_TYPE = 703
def __init__(
self,
public_key: bytes = None,
) -> None:
self.public_key = public_key
@classmethod
def get_fields(cls):
return {
1: ('public_key', p.BytesType, 0),
}

@ -0,0 +1,43 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
if __debug__:
try:
from typing import List
except ImportError:
List = None # type: ignore
class BinanceSignTx(p.MessageType):
MESSAGE_WIRE_TYPE = 704
def __init__(
self,
address_n: List[int] = None,
msg_count: int = None,
account_number: int = None,
chain_id: str = None,
memo: str = None,
sequence: int = None,
source: int = None,
) -> None:
self.address_n = address_n if address_n is not None else []
self.msg_count = msg_count
self.account_number = account_number
self.chain_id = chain_id
self.memo = memo
self.sequence = sequence
self.source = source
@classmethod
def get_fields(cls):
return {
1: ('address_n', p.UVarintType, p.FLAG_REPEATED),
2: ('msg_count', p.UVarintType, 0),
3: ('account_number', p.SVarintType, 0),
4: ('chain_id', p.UnicodeType, 0),
5: ('memo', p.UnicodeType, 0),
6: ('sequence', p.SVarintType, 0),
7: ('source', p.SVarintType, 0),
}

@ -0,0 +1,25 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
class BinanceSignedTx(p.MessageType):
MESSAGE_WIRE_TYPE = 709
def __init__(
self,
signature: bytes = None,
public_key: bytes = None,
json: str = None,
) -> None:
self.signature = signature
self.public_key = public_key
self.json = json
@classmethod
def get_fields(cls):
return {
1: ('signature', p.BytesType, 0),
2: ('public_key', p.BytesType, 0),
3: ('json', p.UnicodeType, 0),
}

@ -0,0 +1,30 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
from .InputOutput import InputOutput
if __debug__:
try:
from typing import List
except ImportError:
List = None # type: ignore
class BinanceTransferMsg(p.MessageType):
MESSAGE_WIRE_TYPE = 706
def __init__(
self,
inputs: List[InputOutput] = None,
outputs: List[InputOutput] = None,
) -> None:
self.inputs = inputs if inputs is not None else []
self.outputs = outputs if outputs is not None else []
@classmethod
def get_fields(cls):
return {
1: ('inputs', InputOutput, p.FLAG_REPEATED),
2: ('outputs', InputOutput, p.FLAG_REPEATED),
}

@ -0,0 +1,7 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
class BinanceTxRequest(p.MessageType):
MESSAGE_WIRE_TYPE = 705

@ -0,0 +1,21 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
class Coin(p.MessageType):
def __init__(
self,
amount: int = None,
denom: str = None,
) -> None:
self.amount = amount
self.denom = denom
@classmethod
def get_fields(cls):
return {
1: ('amount', p.SVarintType, 0),
2: ('denom', p.UnicodeType, 0),
}

@ -13,7 +13,7 @@ class DebugLinkState(p.MessageType):
layout: bytes = None,
pin: str = None,
matrix: str = None,
mnemonic: str = None,
mnemonic_secret: bytes = None,
node: HDNodeType = None,
passphrase_protection: bool = None,
reset_word: str = None,
@ -21,11 +21,12 @@ class DebugLinkState(p.MessageType):
recovery_fake_word: str = None,
recovery_word_pos: int = None,
reset_word_pos: int = None,
mnemonic_type: int = None,
) -> None:
self.layout = layout
self.pin = pin
self.matrix = matrix
self.mnemonic = mnemonic
self.mnemonic_secret = mnemonic_secret
self.node = node
self.passphrase_protection = passphrase_protection
self.reset_word = reset_word
@ -33,6 +34,7 @@ class DebugLinkState(p.MessageType):
self.recovery_fake_word = recovery_fake_word
self.recovery_word_pos = recovery_word_pos
self.reset_word_pos = reset_word_pos
self.mnemonic_type = mnemonic_type
@classmethod
def get_fields(cls):
@ -40,7 +42,7 @@ class DebugLinkState(p.MessageType):
1: ('layout', p.BytesType, 0),
2: ('pin', p.UnicodeType, 0),
3: ('matrix', p.UnicodeType, 0),
4: ('mnemonic', p.UnicodeType, 0),
4: ('mnemonic_secret', p.BytesType, 0),
5: ('node', HDNodeType, 0),
6: ('passphrase_protection', p.BoolType, 0),
7: ('reset_word', p.UnicodeType, 0),
@ -48,4 +50,5 @@ class DebugLinkState(p.MessageType):
9: ('recovery_fake_word', p.UnicodeType, 0),
10: ('recovery_word_pos', p.UVarintType, 0),
11: ('reset_word_pos', p.UVarintType, 0),
12: ('mnemonic_type', p.UVarintType, 0),
}

@ -0,0 +1,29 @@
# Automatically generated by pb2py
# fmt: off
import protobuf as p
from .Coin import Coin
if __debug__:
try:
from typing import List
except ImportError:
List = None # type: ignore
class InputOutput(p.MessageType):
def __init__(
self,
address: str = None,
coins: List[Coin] = None,
) -> None:
self.address = address
self.coins = coins if coins is not None else []
@classmethod
def get_fields(cls):
return {
1: ('address', p.UnicodeType, 0),
2: ('coins', Coin, p.FLAG_REPEATED),
}

@ -183,3 +183,13 @@ EosSignTx = 602
EosTxActionRequest = 603
EosTxActionAck = 604
EosSignedTx = 605
BinanceGetAddress = 700
BinanceAddress = 701
BinanceGetPublicKey = 702
BinancePublicKey = 703
BinanceSignTx = 704
BinanceTxRequest = 705
BinanceTransferMsg = 706
BinanceOrderMsg = 707
BinanceCancelMsg = 708
BinanceSignedTx = 709

@ -0,0 +1,5 @@
# Automatically generated by pb2py
# fmt: off
SIDE_UNKNOWN = 0
BUY = 1
SELL = 2

@ -0,0 +1,6 @@
# Automatically generated by pb2py
# fmt: off
OT_UNKNOWN = 0
MARKET = 1
LIMIT = 2
OT_RESERVED = 3

@ -19,6 +19,6 @@ class PublicKey(p.MessageType):
@classmethod
def get_fields(cls):
return {
1: ('node', HDNodeType, 0), # required
1: ('node', HDNodeType, 0),
2: ('xpub', p.UnicodeType, 0),
}

@ -0,0 +1,6 @@
# Automatically generated by pb2py
# fmt: off
TIF_UNKNOWN = 0
GTE = 1
TIF_RESERVED = 2
IOC = 3

@ -1 +1 @@
Subproject commit cb238cb1f134accc4200217d9511115a8f61c6cb
Subproject commit c5e54d7535c8772b9a75ff90c506a28526f94267
Loading…
Cancel
Save