commit
5b11d9a65d
@ -0,0 +1,193 @@
|
|||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from trezor import io, ui, wire
|
||||||
|
from trezor.crypto import hmac
|
||||||
|
from trezor.crypto.hashlib import sha256
|
||||||
|
from trezor.ui.confirm import Confirm
|
||||||
|
from trezor.ui.text import Text
|
||||||
|
from trezor.utils import consteq
|
||||||
|
|
||||||
|
from apps.common import storage
|
||||||
|
from apps.common.confirm import require_confirm
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class SdProtectCancelled(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
SD_SALT_LEN_BYTES = const(32)
|
||||||
|
SD_SALT_AUTH_TAG_LEN_BYTES = const(16)
|
||||||
|
SD_SALT_AUTH_KEY_LEN_BYTES = const(16)
|
||||||
|
|
||||||
|
|
||||||
|
async def wrong_card_dialog(ctx: Optional[wire.Context]) -> None:
|
||||||
|
text = Text("SD card protection", ui.ICON_WRONG)
|
||||||
|
text.bold("Wrong SD card.")
|
||||||
|
text.br_half()
|
||||||
|
text.normal("Please unplug the", "device and insert a", "different card.")
|
||||||
|
if ctx is None:
|
||||||
|
await Confirm(text, confirm=None)
|
||||||
|
else:
|
||||||
|
await require_confirm(ctx, text, confirm=None)
|
||||||
|
|
||||||
|
|
||||||
|
async def insert_card_dialog(ctx: Optional[wire.Context]) -> None:
|
||||||
|
text = Text("SD card protection")
|
||||||
|
text.bold("SD card required.")
|
||||||
|
text.br_half()
|
||||||
|
text.normal("Please unplug the", "device and insert your", "SD card.")
|
||||||
|
if ctx is None:
|
||||||
|
await Confirm(text, confirm=None)
|
||||||
|
else:
|
||||||
|
await require_confirm(ctx, text, confirm=None)
|
||||||
|
|
||||||
|
|
||||||
|
async def request_sd_salt(
|
||||||
|
ctx: Optional[wire.Context], salt_auth_key: bytes
|
||||||
|
) -> bytearray:
|
||||||
|
device_dir = "/trezor/device_%s" % storage.device.get_device_id()
|
||||||
|
salt_path = "%s/salt" % device_dir
|
||||||
|
new_salt_path = "%s/salt.new" % device_dir
|
||||||
|
|
||||||
|
sd = io.SDCard()
|
||||||
|
fs = io.FatFS()
|
||||||
|
if not sd.power(True):
|
||||||
|
await insert_card_dialog(ctx)
|
||||||
|
raise SdProtectCancelled
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs.mount()
|
||||||
|
|
||||||
|
# Load salt if it exists.
|
||||||
|
try:
|
||||||
|
with fs.open(salt_path, "r") as f:
|
||||||
|
salt = bytearray(SD_SALT_LEN_BYTES) # type: Optional[bytearray]
|
||||||
|
salt_tag = bytearray(SD_SALT_AUTH_TAG_LEN_BYTES)
|
||||||
|
f.read(salt)
|
||||||
|
f.read(salt_tag)
|
||||||
|
except OSError:
|
||||||
|
salt = None
|
||||||
|
|
||||||
|
if salt is not None and consteq(
|
||||||
|
hmac.new(salt_auth_key, salt, sha256).digest()[:SD_SALT_AUTH_TAG_LEN_BYTES],
|
||||||
|
salt_tag,
|
||||||
|
):
|
||||||
|
return salt
|
||||||
|
|
||||||
|
# Load salt.new if it exists.
|
||||||
|
try:
|
||||||
|
with fs.open(new_salt_path, "r") as f:
|
||||||
|
new_salt = bytearray(SD_SALT_LEN_BYTES) # type: Optional[bytearray]
|
||||||
|
new_salt_tag = bytearray(SD_SALT_AUTH_TAG_LEN_BYTES)
|
||||||
|
f.read(new_salt)
|
||||||
|
f.read(new_salt_tag)
|
||||||
|
except OSError:
|
||||||
|
new_salt = None
|
||||||
|
|
||||||
|
if new_salt is not None and consteq(
|
||||||
|
hmac.new(salt_auth_key, new_salt, sha256).digest()[
|
||||||
|
:SD_SALT_AUTH_TAG_LEN_BYTES
|
||||||
|
],
|
||||||
|
new_salt_tag,
|
||||||
|
):
|
||||||
|
# SD salt regeneration was interrupted earlier. Bring into consistent state.
|
||||||
|
# TODO Possibly overwrite salt file with random data.
|
||||||
|
try:
|
||||||
|
fs.unlink(salt_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
fs.rename(new_salt_path, salt_path)
|
||||||
|
return new_salt
|
||||||
|
finally:
|
||||||
|
fs.unmount()
|
||||||
|
sd.power(False)
|
||||||
|
|
||||||
|
await wrong_card_dialog(ctx)
|
||||||
|
raise SdProtectCancelled
|
||||||
|
|
||||||
|
|
||||||
|
async def set_sd_salt(
|
||||||
|
ctx: Optional[wire.Context], salt: bytes, salt_tag: bytes, filename: str = "salt"
|
||||||
|
) -> None:
|
||||||
|
device_dir = "/trezor/device_%s" % storage.device.get_device_id()
|
||||||
|
salt_path = "%s/%s" % (device_dir, filename)
|
||||||
|
|
||||||
|
sd = io.SDCard()
|
||||||
|
fs = io.FatFS()
|
||||||
|
if not sd.power(True):
|
||||||
|
await insert_card_dialog(ctx)
|
||||||
|
raise SdProtectCancelled
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs.mount()
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs.mkdir("/trezor")
|
||||||
|
except OSError:
|
||||||
|
# Directory already exists.
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs.mkdir(device_dir)
|
||||||
|
except OSError:
|
||||||
|
# Directory already exists.
|
||||||
|
pass
|
||||||
|
|
||||||
|
with fs.open(salt_path, "w") as f:
|
||||||
|
f.write(salt)
|
||||||
|
f.write(salt_tag)
|
||||||
|
finally:
|
||||||
|
fs.unmount()
|
||||||
|
sd.power(False)
|
||||||
|
|
||||||
|
|
||||||
|
async def stage_sd_salt(
|
||||||
|
ctx: Optional[wire.Context], salt: bytes, salt_tag: bytes
|
||||||
|
) -> None:
|
||||||
|
await set_sd_salt(ctx, salt, salt_tag, "salt.new")
|
||||||
|
|
||||||
|
|
||||||
|
async def commit_sd_salt(ctx: Optional[wire.Context]) -> None:
|
||||||
|
device_dir = "/trezor/device_%s" % storage.device.get_device_id()
|
||||||
|
salt_path = "%s/salt" % device_dir
|
||||||
|
new_salt_path = "%s/salt.new" % device_dir
|
||||||
|
|
||||||
|
sd = io.SDCard()
|
||||||
|
fs = io.FatFS()
|
||||||
|
if not sd.power(True):
|
||||||
|
await insert_card_dialog(ctx)
|
||||||
|
raise SdProtectCancelled
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs.mount()
|
||||||
|
# TODO Possibly overwrite salt file with random data.
|
||||||
|
try:
|
||||||
|
fs.unlink(salt_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
fs.rename(new_salt_path, salt_path)
|
||||||
|
finally:
|
||||||
|
fs.unmount()
|
||||||
|
sd.power(False)
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_sd_salt(ctx: Optional[wire.Context]) -> None:
|
||||||
|
device_dir = "/trezor/device_%s" % storage.device.get_device_id()
|
||||||
|
salt_path = "%s/salt" % device_dir
|
||||||
|
|
||||||
|
sd = io.SDCard()
|
||||||
|
fs = io.FatFS()
|
||||||
|
if not sd.power(True):
|
||||||
|
await insert_card_dialog(ctx)
|
||||||
|
raise SdProtectCancelled
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs.mount()
|
||||||
|
# TODO Possibly overwrite salt file with random data.
|
||||||
|
fs.unlink(salt_path)
|
||||||
|
finally:
|
||||||
|
fs.unmount()
|
||||||
|
sd.power(False)
|
@ -0,0 +1,169 @@
|
|||||||
|
from trezor import config, ui, wire
|
||||||
|
from trezor.crypto import hmac, random
|
||||||
|
from trezor.crypto.hashlib import sha256
|
||||||
|
from trezor.messages import SdProtectOperationType
|
||||||
|
from trezor.messages.Success import Success
|
||||||
|
from trezor.pin import pin_to_int
|
||||||
|
from trezor.ui.text import Text
|
||||||
|
|
||||||
|
from apps.common.confirm import require_confirm
|
||||||
|
from apps.common.request_pin import request_pin_ack, request_pin_and_sd_salt
|
||||||
|
from apps.common.sd_salt import (
|
||||||
|
SD_SALT_AUTH_KEY_LEN_BYTES,
|
||||||
|
SD_SALT_AUTH_TAG_LEN_BYTES,
|
||||||
|
SD_SALT_LEN_BYTES,
|
||||||
|
commit_sd_salt,
|
||||||
|
remove_sd_salt,
|
||||||
|
set_sd_salt,
|
||||||
|
stage_sd_salt,
|
||||||
|
)
|
||||||
|
from apps.common.storage import device, is_initialized
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from trezor.messages.SdProtect import SdProtect
|
||||||
|
|
||||||
|
|
||||||
|
async def sd_protect(ctx: wire.Context, msg: SdProtect) -> Success:
|
||||||
|
if not is_initialized():
|
||||||
|
raise wire.ProcessError("Device is not initialized")
|
||||||
|
|
||||||
|
if msg.operation == SdProtectOperationType.ENABLE:
|
||||||
|
return await sd_protect_enable(ctx, msg)
|
||||||
|
elif msg.operation == SdProtectOperationType.DISABLE:
|
||||||
|
return await sd_protect_disable(ctx, msg)
|
||||||
|
elif msg.operation == SdProtectOperationType.REFRESH:
|
||||||
|
return await sd_protect_refresh(ctx, msg)
|
||||||
|
else:
|
||||||
|
raise wire.ProcessError("Unknown operation")
|
||||||
|
|
||||||
|
|
||||||
|
async def sd_protect_enable(ctx: wire.Context, msg: SdProtect) -> Success:
|
||||||
|
salt_auth_key = device.get_sd_salt_auth_key()
|
||||||
|
if salt_auth_key is not None:
|
||||||
|
raise wire.ProcessError("SD card protection already enabled")
|
||||||
|
|
||||||
|
# Confirm that user wants to proceed with the operation.
|
||||||
|
await require_confirm_sd_protect(ctx, msg)
|
||||||
|
|
||||||
|
# Get the current PIN.
|
||||||
|
if config.has_pin():
|
||||||
|
pin = pin_to_int(await request_pin_ack(ctx, "Enter PIN", config.get_pin_rem()))
|
||||||
|
else:
|
||||||
|
pin = pin_to_int("")
|
||||||
|
|
||||||
|
# Check PIN and prepare salt file.
|
||||||
|
salt = random.bytes(SD_SALT_LEN_BYTES)
|
||||||
|
salt_auth_key = random.bytes(SD_SALT_AUTH_KEY_LEN_BYTES)
|
||||||
|
salt_tag = hmac.new(salt_auth_key, salt, sha256).digest()[
|
||||||
|
:SD_SALT_AUTH_TAG_LEN_BYTES
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
await set_sd_salt(ctx, salt, salt_tag)
|
||||||
|
except Exception:
|
||||||
|
raise wire.ProcessError("Failed to write to SD card")
|
||||||
|
|
||||||
|
if not config.change_pin(pin, pin, None, salt):
|
||||||
|
# Wrong PIN. Clean up the prepared salt file.
|
||||||
|
try:
|
||||||
|
await remove_sd_salt(ctx)
|
||||||
|
except Exception:
|
||||||
|
# The cleanup is not necessary for the correct functioning of
|
||||||
|
# SD-protection. If it fails for any reason, we suppress the
|
||||||
|
# exception, because primarily we need to raise wire.PinInvalid.
|
||||||
|
pass
|
||||||
|
raise wire.PinInvalid("PIN invalid")
|
||||||
|
|
||||||
|
device.set_sd_salt_auth_key(salt_auth_key)
|
||||||
|
|
||||||
|
return Success(message="SD card protection enabled")
|
||||||
|
|
||||||
|
|
||||||
|
async def sd_protect_disable(ctx: wire.Context, msg: SdProtect) -> Success:
|
||||||
|
if device.get_sd_salt_auth_key() is None:
|
||||||
|
raise wire.ProcessError("SD card protection not enabled")
|
||||||
|
|
||||||
|
# Confirm that user wants to proceed with the operation.
|
||||||
|
await require_confirm_sd_protect(ctx, msg)
|
||||||
|
|
||||||
|
# Get the current PIN and salt from the SD card.
|
||||||
|
pin, salt = await request_pin_and_sd_salt(ctx, "Enter PIN")
|
||||||
|
|
||||||
|
# Check PIN and remove salt.
|
||||||
|
if not config.change_pin(pin_to_int(pin), pin_to_int(pin), salt, None):
|
||||||
|
raise wire.PinInvalid("PIN invalid")
|
||||||
|
|
||||||
|
device.set_sd_salt_auth_key(None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Clean up.
|
||||||
|
await remove_sd_salt(ctx)
|
||||||
|
except Exception:
|
||||||
|
# The cleanup is not necessary for the correct functioning of
|
||||||
|
# SD-protection. If it fails for any reason, we suppress the exception,
|
||||||
|
# because overall SD-protection was successfully disabled.
|
||||||
|
pass
|
||||||
|
|
||||||
|
return Success(message="SD card protection disabled")
|
||||||
|
|
||||||
|
|
||||||
|
async def sd_protect_refresh(ctx: wire.Context, msg: SdProtect) -> Success:
|
||||||
|
if device.get_sd_salt_auth_key() is None:
|
||||||
|
raise wire.ProcessError("SD card protection not enabled")
|
||||||
|
|
||||||
|
# Confirm that user wants to proceed with the operation.
|
||||||
|
await require_confirm_sd_protect(ctx, msg)
|
||||||
|
|
||||||
|
# Get the current PIN and salt from the SD card.
|
||||||
|
pin, old_salt = await request_pin_and_sd_salt(ctx, "Enter PIN")
|
||||||
|
|
||||||
|
# Check PIN and change salt.
|
||||||
|
new_salt = random.bytes(SD_SALT_LEN_BYTES)
|
||||||
|
new_salt_auth_key = random.bytes(SD_SALT_AUTH_KEY_LEN_BYTES)
|
||||||
|
new_salt_tag = hmac.new(new_salt_auth_key, new_salt, sha256).digest()[
|
||||||
|
:SD_SALT_AUTH_TAG_LEN_BYTES
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
await stage_sd_salt(ctx, new_salt, new_salt_tag)
|
||||||
|
except Exception:
|
||||||
|
raise wire.ProcessError("Failed to write to SD card")
|
||||||
|
|
||||||
|
if not config.change_pin(pin_to_int(pin), pin_to_int(pin), old_salt, new_salt):
|
||||||
|
raise wire.PinInvalid("PIN invalid")
|
||||||
|
|
||||||
|
device.set_sd_salt_auth_key(new_salt_auth_key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Clean up.
|
||||||
|
await commit_sd_salt(ctx)
|
||||||
|
except Exception:
|
||||||
|
# If the cleanup fails, then request_sd_salt() will bring the SD card
|
||||||
|
# into a consistent state. We suppress the exception, because overall
|
||||||
|
# SD-protection was successfully refreshed.
|
||||||
|
pass
|
||||||
|
|
||||||
|
return Success(message="SD card protection refreshed")
|
||||||
|
|
||||||
|
|
||||||
|
def require_confirm_sd_protect(ctx: wire.Context, msg: SdProtect) -> None:
|
||||||
|
if msg.operation == SdProtectOperationType.ENABLE:
|
||||||
|
text = Text("SD card protection", ui.ICON_CONFIG)
|
||||||
|
text.normal(
|
||||||
|
"Do you really want to", "secure your device with", "SD card protection?"
|
||||||
|
)
|
||||||
|
elif msg.operation == SdProtectOperationType.DISABLE:
|
||||||
|
text = Text("SD card protection", ui.ICON_CONFIG)
|
||||||
|
text.normal(
|
||||||
|
"Do you really want to", "remove SD card", "protection from your", "device?"
|
||||||
|
)
|
||||||
|
elif msg.operation == SdProtectOperationType.REFRESH:
|
||||||
|
text = Text("SD card protection", ui.ICON_CONFIG)
|
||||||
|
text.normal(
|
||||||
|
"Do you really want to",
|
||||||
|
"replace the current",
|
||||||
|
"SD card secret with a",
|
||||||
|
"newly generated one?",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise wire.ProcessError("Unknown operation")
|
||||||
|
|
||||||
|
return require_confirm(ctx, text)
|
@ -0,0 +1,28 @@
|
|||||||
|
# Automatically generated by pb2py
|
||||||
|
# fmt: off
|
||||||
|
import protobuf as p
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
try:
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from typing_extensions import Literal # noqa: F401
|
||||||
|
EnumTypeSdProtectOperationType = Literal[0, 1, 2]
|
||||||
|
except ImportError:
|
||||||
|
Dict, List, Optional = None, None, None # type: ignore
|
||||||
|
EnumTypeSdProtectOperationType = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class SdProtect(p.MessageType):
|
||||||
|
MESSAGE_WIRE_TYPE = 79
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
operation: EnumTypeSdProtectOperationType = None,
|
||||||
|
) -> None:
|
||||||
|
self.operation = operation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls) -> Dict:
|
||||||
|
return {
|
||||||
|
1: ('operation', p.EnumType("SdProtectOperationType", (0, 1, 2)), 0),
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
# Automatically generated by pb2py
|
||||||
|
# fmt: off
|
||||||
|
DISABLE = 0
|
||||||
|
ENABLE = 1
|
||||||
|
REFRESH = 2
|
@ -0,0 +1,28 @@
|
|||||||
|
# Automatically generated by pb2py
|
||||||
|
# fmt: off
|
||||||
|
from .. import protobuf as p
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
try:
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from typing_extensions import Literal # noqa: F401
|
||||||
|
EnumTypeSdProtectOperationType = Literal[0, 1, 2]
|
||||||
|
except ImportError:
|
||||||
|
Dict, List, Optional = None, None, None # type: ignore
|
||||||
|
EnumTypeSdProtectOperationType = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class SdProtect(p.MessageType):
|
||||||
|
MESSAGE_WIRE_TYPE = 79
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
operation: EnumTypeSdProtectOperationType = None,
|
||||||
|
) -> None:
|
||||||
|
self.operation = operation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls) -> Dict:
|
||||||
|
return {
|
||||||
|
1: ('operation', p.EnumType("SdProtectOperationType", (0, 1, 2)), 0),
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
# Automatically generated by pb2py
|
||||||
|
# fmt: off
|
||||||
|
DISABLE = 0
|
||||||
|
ENABLE = 1
|
||||||
|
REFRESH = 2
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,62 @@
|
|||||||
|
# This file is part of the Trezor project.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012-2019 SatoshiLabs and contributors
|
||||||
|
#
|
||||||
|
# This library is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License version 3
|
||||||
|
# as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the License along with this library.
|
||||||
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from trezorlib import debuglink, device, messages as proto
|
||||||
|
from trezorlib.exceptions import TrezorFailure
|
||||||
|
|
||||||
|
from ..common import MNEMONIC12
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_t1
|
||||||
|
class TestMsgSdProtect:
|
||||||
|
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
|
||||||
|
def test_sd_protect(self, client):
|
||||||
|
|
||||||
|
# Disabling SD protection should fail
|
||||||
|
with pytest.raises(TrezorFailure):
|
||||||
|
device.sd_protect(client, proto.SdProtectOperationType.DISABLE)
|
||||||
|
|
||||||
|
# Enable SD protection
|
||||||
|
device.sd_protect(client, proto.SdProtectOperationType.ENABLE)
|
||||||
|
|
||||||
|
# Enabling SD protection should fail
|
||||||
|
with pytest.raises(TrezorFailure):
|
||||||
|
device.sd_protect(client, proto.SdProtectOperationType.ENABLE)
|
||||||
|
|
||||||
|
# Wipe
|
||||||
|
device.wipe(client)
|
||||||
|
debuglink.load_device_by_mnemonic(
|
||||||
|
client,
|
||||||
|
mnemonic=MNEMONIC12,
|
||||||
|
pin="",
|
||||||
|
passphrase_protection=False,
|
||||||
|
label="test",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable SD protection
|
||||||
|
device.sd_protect(client, proto.SdProtectOperationType.ENABLE)
|
||||||
|
|
||||||
|
# Refresh SD protection
|
||||||
|
device.sd_protect(client, proto.SdProtectOperationType.REFRESH)
|
||||||
|
|
||||||
|
# Disable SD protection
|
||||||
|
device.sd_protect(client, proto.SdProtectOperationType.DISABLE)
|
||||||
|
|
||||||
|
# Refreshing SD protection should fail
|
||||||
|
with pytest.raises(TrezorFailure):
|
||||||
|
device.sd_protect(client, proto.SdProtectOperationType.REFRESH)
|
Loading…
Reference in new issue