From 4f010036374d6d97f438bb565b3d59d6073ee29a Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Fri, 9 Aug 2019 22:52:09 +0200 Subject: [PATCH 1/8] storage: Add external salt parameter to unlock() and change_pin(). --- .../extmod/modtrezorconfig/modtrezorconfig.c | 78 +++++++++++++----- core/mocks/generated/trezorconfig.pyi | 20 +++-- legacy/firmware/config.c | 13 +-- storage/storage.c | 55 +++++++----- storage/storage.h | 9 +- storage/tests/c/libtrezor-storage.so | Bin 86616 -> 78632 bytes storage/tests/c/storage.py | 21 ++++- storage/tests/c0/libtrezor-storage0.so | Bin 21928 -> 18032 bytes 8 files changed, 134 insertions(+), 62 deletions(-) diff --git a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c index 43572e78b..3c6356883 100644 --- a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c +++ b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c @@ -67,30 +67,41 @@ STATIC mp_obj_t mod_trezorconfig_init(size_t n_args, const mp_obj_t *args) { STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_init_obj, 0, 1, mod_trezorconfig_init); -/// def unlock(pin: int) -> bool: +/// def unlock(pin: int, ext_salt: Optional[bytes] = None) -> bool: /// """ -/// Attempts to unlock the storage with given PIN. Returns True on -/// success, False on failure. +/// Attempts to unlock the storage with the given PIN and external salt. +/// Returns True on success, False on failure. /// """ -STATIC mp_obj_t mod_trezorconfig_unlock(mp_obj_t pin) { - uint32_t pin_i = trezor_obj_get_uint(pin); - if (sectrue != storage_unlock(pin_i)) { +STATIC mp_obj_t mod_trezorconfig_unlock(size_t n_args, const mp_obj_t *args) { + uint32_t pin = trezor_obj_get_uint(args[0]); + const uint8_t *ext_salt = NULL; + if (n_args > 1 && args[1] != mp_const_none) { + mp_buffer_info_t ext_salt_b; + mp_get_buffer_raise(args[1], &ext_salt_b, MP_BUFFER_READ); + if (ext_salt_b.len != EXTERNAL_SALT_SIZE) + mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt."); + ext_salt = ext_salt_b.buf; + } + + if (sectrue != storage_unlock(pin, ext_salt)) { return mp_const_false; } return mp_const_true; } -STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorconfig_unlock_obj, - mod_trezorconfig_unlock); +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_unlock_obj, 1, 2, + mod_trezorconfig_unlock); -/// def check_pin(pin: int) -> bool: +/// def check_pin(pin: int, ext_salt: Optional[bytes] = None) -> bool: /// """ -/// Check the given PIN. Returns True on success, False on failure. +/// Check the given PIN with the given external salt. +/// Returns True on success, False on failure. /// """ -STATIC mp_obj_t mod_trezorconfig_check_pin(mp_obj_t pin) { - return mod_trezorconfig_unlock(pin); +STATIC mp_obj_t mod_trezorconfig_check_pin(size_t n_args, + const mp_obj_t *args) { + return mod_trezorconfig_unlock(n_args, args); } -STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorconfig_check_pin_obj, - mod_trezorconfig_check_pin); +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_check_pin_obj, 1, 2, + mod_trezorconfig_check_pin); /// def lock() -> None: /// """ @@ -126,20 +137,43 @@ STATIC mp_obj_t mod_trezorconfig_get_pin_rem(void) { STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorconfig_get_pin_rem_obj, mod_trezorconfig_get_pin_rem); -/// def change_pin(pin: int, newpin: int) -> bool: +/// def change_pin( +/// oldpin: int, +/// newpin: int, +/// old_ext_salt: Optional[bytes] = None, +/// new_ext_salt: Optional[bytes] = None, +/// ) -> bool: /// """ -/// Change PIN. Returns True on success, False on failure. +/// Change PIN and external salt. Returns True on success, False on failure. /// """ -STATIC mp_obj_t mod_trezorconfig_change_pin(mp_obj_t pin, mp_obj_t newpin) { - uint32_t pin_i = trezor_obj_get_uint(pin); - uint32_t newpin_i = trezor_obj_get_uint(newpin); - if (sectrue != storage_change_pin(pin_i, newpin_i)) { +STATIC mp_obj_t mod_trezorconfig_change_pin(size_t n_args, + const mp_obj_t *args) { + uint32_t oldpin = trezor_obj_get_uint(args[0]); + uint32_t newpin = trezor_obj_get_uint(args[1]); + mp_buffer_info_t ext_salt_b; + const uint8_t *old_ext_salt = NULL; + if (n_args > 2 && args[2] != mp_const_none) { + mp_get_buffer_raise(args[2], &ext_salt_b, MP_BUFFER_READ); + if (ext_salt_b.len != EXTERNAL_SALT_SIZE) + mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt."); + old_ext_salt = ext_salt_b.buf; + } + const uint8_t *new_ext_salt = NULL; + if (n_args > 3 && args[3] != mp_const_none) { + mp_get_buffer_raise(args[3], &ext_salt_b, MP_BUFFER_READ); + if (ext_salt_b.len != EXTERNAL_SALT_SIZE) + mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt."); + new_ext_salt = ext_salt_b.buf; + } + + if (sectrue != + storage_change_pin(oldpin, newpin, old_ext_salt, new_ext_salt)) { return mp_const_false; } return mp_const_true; } -STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorconfig_change_pin_obj, - mod_trezorconfig_change_pin); +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_change_pin_obj, 2, + 4, mod_trezorconfig_change_pin); /// def get(app: int, key: int, public: bool = False) -> Optional[bytes]: /// """ diff --git a/core/mocks/generated/trezorconfig.pyi b/core/mocks/generated/trezorconfig.pyi index 399ea4d95..ae3742467 100644 --- a/core/mocks/generated/trezorconfig.pyi +++ b/core/mocks/generated/trezorconfig.pyi @@ -12,17 +12,18 @@ def init( # extmod/modtrezorconfig/modtrezorconfig.c -def unlock(pin: int) -> bool: +def unlock(pin: int, ext_salt: Optional[bytes] = None) -> bool: """ - Attempts to unlock the storage with given PIN. Returns True on - success, False on failure. + Attempts to unlock the storage with the given PIN and external salt. + Returns True on success, False on failure. """ # extmod/modtrezorconfig/modtrezorconfig.c -def check_pin(pin: int) -> bool: +def check_pin(pin: int, ext_salt: Optional[bytes] = None) -> bool: """ - Check the given PIN. Returns True on success, False on failure. + Check the given PIN with the given external salt. + Returns True on success, False on failure. """ @@ -48,9 +49,14 @@ def get_pin_rem() -> int: # extmod/modtrezorconfig/modtrezorconfig.c -def change_pin(pin: int, newpin: int) -> bool: +def change_pin( + oldpin: int, + newpin: int, + old_ext_salt: Optional[bytes] = None, + new_ext_salt: Optional[bytes] = None, +) -> bool: """ - Change PIN. Returns True on success, False on failure. + Change PIN and external salt. Returns True on success, False on failure. """ diff --git a/legacy/firmware/config.c b/legacy/firmware/config.c index 16a206d46..6272cff0c 100644 --- a/legacy/firmware/config.c +++ b/legacy/firmware/config.c @@ -316,9 +316,9 @@ static secbool config_upgrade_v10(void) { } storage_init(NULL, HW_ENTROPY_DATA, HW_ENTROPY_LEN); - storage_unlock(PIN_EMPTY); + storage_unlock(PIN_EMPTY, NULL); if (config.has_pin) { - storage_change_pin(PIN_EMPTY, pin_to_int(config.pin)); + storage_change_pin(PIN_EMPTY, pin_to_int(config.pin), NULL, NULL); } while (pin_wait != 0) { @@ -386,7 +386,7 @@ void config_init(void) { // Auto-unlock storage if no PIN is set. if (storage_is_unlocked() == secfalse && storage_has_pin() == secfalse) { - storage_unlock(PIN_EMPTY); + storage_unlock(PIN_EMPTY, NULL); } uint16_t len = 0; @@ -759,7 +759,7 @@ bool config_containsMnemonic(const char *mnemonic) { */ bool config_unlock(const char *pin) { char oldTiny = usbTiny(1); - secbool ret = storage_unlock(pin_to_int(pin)); + secbool ret = storage_unlock(pin_to_int(pin), NULL); usbTiny(oldTiny); return sectrue == ret; } @@ -773,7 +773,8 @@ bool config_changePin(const char *old_pin, const char *new_pin) { } char oldTiny = usbTiny(1); - secbool ret = storage_change_pin(pin_to_int(old_pin), new_pin_int); + secbool ret = + storage_change_pin(pin_to_int(old_pin), new_pin_int, NULL, NULL); usbTiny(oldTiny); #if DEBUG_LINK @@ -925,7 +926,7 @@ void config_wipe(void) { char oldTiny = usbTiny(1); storage_wipe(); if (storage_is_unlocked() != sectrue) { - storage_unlock(PIN_EMPTY); + storage_unlock(PIN_EMPTY, NULL); } usbTiny(oldTiny); random_buffer((uint8_t *)config_uuid, sizeof(config_uuid)); diff --git a/storage/storage.c b/storage/storage.c index 31c10898b..50ef07e01 100644 --- a/storage/storage.c +++ b/storage/storage.c @@ -331,15 +331,26 @@ static secbool auth_get(uint16_t key, const void **val, uint16_t *len) { } static void derive_kek(uint32_t pin, const uint8_t *random_salt, + const uint8_t *ext_salt, uint8_t kek[SHA256_DIGEST_LENGTH], uint8_t keiv[SHA256_DIGEST_LENGTH]) { #if BYTE_ORDER == BIG_ENDIAN REVERSE32(pin, pin); #endif - uint8_t salt[HARDWARE_SALT_SIZE + RANDOM_SALT_SIZE]; - memcpy(salt, hardware_salt, HARDWARE_SALT_SIZE); - memcpy(salt + HARDWARE_SALT_SIZE, random_salt, RANDOM_SALT_SIZE); + uint8_t salt[HARDWARE_SALT_SIZE + RANDOM_SALT_SIZE + EXTERNAL_SALT_SIZE]; + size_t salt_len = 0; + + memcpy(salt + salt_len, hardware_salt, HARDWARE_SALT_SIZE); + salt_len += HARDWARE_SALT_SIZE; + + memcpy(salt + salt_len, random_salt, RANDOM_SALT_SIZE); + salt_len += RANDOM_SALT_SIZE; + + if (ext_salt != NULL) { + memcpy(salt + salt_len, ext_salt, EXTERNAL_SALT_SIZE); + salt_len += EXTERNAL_SALT_SIZE; + } uint32_t progress = (ui_total - ui_rem) * 1000 / ui_total; if (ui_callback && ui_message) { @@ -348,7 +359,7 @@ static void derive_kek(uint32_t pin, const uint8_t *random_salt, PBKDF2_HMAC_SHA256_CTX ctx; pbkdf2_hmac_sha256_Init(&ctx, (const uint8_t *)&pin, sizeof(pin), salt, - sizeof(salt), 1); + salt_len, 1); for (int i = 1; i <= 5; i++) { pbkdf2_hmac_sha256_Update(&ctx, PIN_ITER_COUNT / 10); if (ui_callback && ui_message) { @@ -360,7 +371,7 @@ static void derive_kek(uint32_t pin, const uint8_t *random_salt, pbkdf2_hmac_sha256_Final(&ctx, kek); pbkdf2_hmac_sha256_Init(&ctx, (const uint8_t *)&pin, sizeof(pin), salt, - sizeof(salt), 2); + salt_len, 2); for (int i = 6; i <= 10; i++) { pbkdf2_hmac_sha256_Update(&ctx, PIN_ITER_COUNT / 10); if (ui_callback && ui_message) { @@ -377,17 +388,17 @@ static void derive_kek(uint32_t pin, const uint8_t *random_salt, memzero(&salt, sizeof(salt)); } -static secbool set_pin(uint32_t pin) { +static secbool set_pin(uint32_t pin, const uint8_t *ext_salt) { uint8_t buffer[RANDOM_SALT_SIZE + KEYS_SIZE + POLY1305_TAG_SIZE]; - uint8_t *salt = buffer; + uint8_t *rand_salt = buffer; uint8_t *ekeys = buffer + RANDOM_SALT_SIZE; uint8_t *pvc = buffer + RANDOM_SALT_SIZE + KEYS_SIZE; uint8_t kek[SHA256_DIGEST_LENGTH]; uint8_t keiv[SHA256_DIGEST_LENGTH]; chacha20poly1305_ctx ctx; - random_buffer(salt, RANDOM_SALT_SIZE); - derive_kek(pin, salt, kek, keiv); + random_buffer(rand_salt, RANDOM_SALT_SIZE); + derive_kek(pin, rand_salt, ext_salt, kek, keiv); rfc7539_init(&ctx, kek, keiv); memzero(kek, sizeof(kek)); memzero(keiv, sizeof(keiv)); @@ -515,7 +526,7 @@ static void init_wiped_storage(void) { ui_total = DERIVE_SECS; ui_rem = ui_total; ui_message = PROCESSING_MSG; - ensure(set_pin(PIN_EMPTY), "init_pin failed"); + ensure(set_pin(PIN_EMPTY, NULL), "init_pin failed"); if (unlocked != sectrue) { memzero(cached_keys, sizeof(cached_keys)); } @@ -784,7 +795,7 @@ static secbool decrypt_dek(const uint8_t *kek, const uint8_t *keiv) { return sectrue; } -static secbool unlock(uint32_t pin) { +static secbool unlock(uint32_t pin, const uint8_t *ext_salt) { if (sectrue != initialized) { return secfalse; } @@ -827,10 +838,10 @@ static secbool unlock(uint32_t pin) { // Read the random salt from EDEK_PVC_KEY and use it to derive the KEK and // KEIV from the PIN. - const void *salt = NULL; + const void *rand_salt = NULL; uint16_t len = 0; if (sectrue != initialized || - sectrue != norcow_get(EDEK_PVC_KEY, &salt, &len) || + sectrue != norcow_get(EDEK_PVC_KEY, &rand_salt, &len) || len != RANDOM_SALT_SIZE + KEYS_SIZE + PVC_SIZE) { memzero(&pin, sizeof(pin)); handle_fault("no EDEK"); @@ -838,7 +849,7 @@ static secbool unlock(uint32_t pin) { } uint8_t kek[SHA256_DIGEST_LENGTH]; uint8_t keiv[SHA256_DIGEST_LENGTH]; - derive_kek(pin, (const uint8_t *)salt, kek, keiv); + derive_kek(pin, (const uint8_t *)rand_salt, ext_salt, kek, keiv); memzero(&pin, sizeof(pin)); // First, we increase PIN fail counter in storage, even before checking the @@ -875,7 +886,7 @@ static secbool unlock(uint32_t pin) { return pin_fails_reset(); } -secbool storage_unlock(uint32_t pin) { +secbool storage_unlock(uint32_t pin, const uint8_t *ext_salt) { ui_total = DERIVE_SECS; ui_rem = ui_total; if (pin == PIN_EMPTY) { @@ -887,7 +898,7 @@ secbool storage_unlock(uint32_t pin) { } else { ui_message = VERIFYING_PIN_MSG; } - return unlock(pin); + return unlock(pin, ext_salt); } /* @@ -1152,7 +1163,9 @@ uint32_t storage_get_pin_rem(void) { return PIN_MAX_TRIES - ctr; } -secbool storage_change_pin(uint32_t oldpin, uint32_t newpin) { +secbool storage_change_pin(uint32_t oldpin, uint32_t newpin, + const uint8_t *old_ext_salt, + const uint8_t *new_ext_salt) { if (sectrue != initialized) { return secfalse; } @@ -1162,10 +1175,10 @@ secbool storage_change_pin(uint32_t oldpin, uint32_t newpin) { ui_message = (oldpin != PIN_EMPTY && newpin == PIN_EMPTY) ? VERIFYING_PIN_MSG : PROCESSING_MSG; - if (sectrue != unlock(oldpin)) { + if (sectrue != unlock(oldpin, old_ext_salt)) { return secfalse; } - secbool ret = set_pin(newpin); + secbool ret = set_pin(newpin, new_ext_salt); memzero(&oldpin, sizeof(oldpin)); memzero(&newpin, sizeof(newpin)); return ret; @@ -1268,9 +1281,9 @@ static secbool storage_upgrade(void) { ui_rem = ui_total; ui_message = PROCESSING_MSG; if (sectrue == norcow_get(V0_PIN_KEY, &val, &len)) { - set_pin(*(const uint32_t *)val); + set_pin(*(const uint32_t *)val, NULL); } else { - set_pin(PIN_EMPTY); + set_pin(PIN_EMPTY, NULL); } // Convert PIN failure counter. diff --git a/storage/storage.h b/storage/storage.h index 2abb409cc..2a8b22375 100644 --- a/storage/storage.h +++ b/storage/storage.h @@ -24,6 +24,9 @@ #include #include "secbool.h" +// The length of the external salt in bytes. +#define EXTERNAL_SALT_SIZE 32 + typedef secbool (*PIN_UI_WAIT_CALLBACK)(uint32_t wait, uint32_t progress, const char *message); @@ -32,11 +35,13 @@ void storage_init(PIN_UI_WAIT_CALLBACK callback, const uint8_t *salt, void storage_wipe(void); secbool storage_is_unlocked(void); void storage_lock(void); -secbool storage_unlock(const uint32_t pin); +secbool storage_unlock(const uint32_t pin, const uint8_t *ext_salt); secbool storage_has_pin(void); secbool storage_pin_fails_increase(void); uint32_t storage_get_pin_rem(void); -secbool storage_change_pin(const uint32_t oldpin, const uint32_t newpin); +secbool storage_change_pin(const uint32_t oldpin, const uint32_t newpin, + const uint8_t *old_ext_salt, + const uint8_t *new_ext_salt); secbool storage_get(const uint16_t key, void *val, const uint16_t max_len, uint16_t *len); secbool storage_set(const uint16_t key, const void *val, uint16_t len); diff --git a/storage/tests/c/libtrezor-storage.so b/storage/tests/c/libtrezor-storage.so index 83aa0728477018e3cca9ef144fa3e5f7cf5b70a9..64898cc473e16081cab5d711043404b7f7d3b08e 100755 GIT binary patch literal 78632 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 X4;8=SsCn1tf3mBzRO7d@5B2|Fg#yV` literal 86616 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 diff --git a/storage/tests/c/storage.py b/storage/tests/c/storage.py index d446b0dcf..51ba59498 100644 --- a/storage/tests/c/storage.py +++ b/storage/tests/c/storage.py @@ -1,6 +1,7 @@ import ctypes as c import os +EXTERNAL_SALT_LEN = 32 sectrue = -1431655766 # 0xAAAAAAAAA fname = os.path.join(os.path.dirname(__file__), "libtrezor-storage.so") @@ -20,8 +21,10 @@ class Storage: def wipe(self) -> None: self.lib.storage_wipe() - def unlock(self, pin: int) -> bool: - return sectrue == self.lib.storage_unlock(c.c_uint32(pin)) + def unlock(self, pin: int, ext_salt: bytes = None) -> bool: + if ext_salt is not None and len(ext_salt) != EXTERNAL_SALT_LEN: + raise ValueError + return sectrue == self.lib.storage_unlock(c.c_uint32(pin), ext_salt) def lock(self) -> None: self.lib.storage_lock() @@ -32,9 +35,19 @@ class Storage: def get_pin_rem(self) -> int: return self.lib.storage_get_pin_rem() - def change_pin(self, oldpin: int, newpin: int) -> bool: + def change_pin( + self, + oldpin: int, + newpin: int, + old_ext_salt: bytes = None, + new_ext_salt: bytes = None, + ) -> bool: + if old_ext_salt is not None and len(old_ext_salt) != EXTERNAL_SALT_LEN: + raise ValueError + if new_ext_salt is not None and len(new_ext_salt) != EXTERNAL_SALT_LEN: + raise ValueError return sectrue == self.lib.storage_change_pin( - c.c_uint32(oldpin), c.c_uint32(newpin) + c.c_uint32(oldpin), c.c_uint32(newpin), old_ext_salt, new_ext_salt ) def get(self, key: int) -> bytes: diff --git a/storage/tests/c0/libtrezor-storage0.so b/storage/tests/c0/libtrezor-storage0.so index e03da87a7d60a1b8c634beab479835dada4ae0c3..3504b9d607d08069419942ee5235895e12995c26 100755 GIT binary patch literal 18032 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 RCx7Rc1jmPpQ9`@?{{cchCT{=$ literal 21928 zcmeHP3wTu3oxhW0Fd#5dfdbYtYG}dLG`s{vG(c{+)C3TjT5IVr$xI+MFOwM_Zlloz zo7-zFcBN(8x@teR&uz>4;PtIEJW3)(jgQjkrfaGxo!LYi7sO~Iv%mj2=YQwUC3JVc z{dV{J_RHnF`QP*VKhFRB&+{JUp8NFTq708m<116UNuyN$B@!u6jQC3R08*eW&_?6$ z<=Rvso0Vdn_f$JZLQJ(BnWj9v^r+HqddUPS$CUY`C-Tdee9c9_H%j76oqEK|zDe@4 zRe6|xFGJ!Tin1OhNpG^!o2>MhwyDrD&6Z|_kL2e9WzS7>l^j!1CSa@o5Dn9FzbcoT z=BoUdvfi!GQ$<&n|1#mQuTlA3C@zC4Kc*@v%12d{iF^6VfVTWQ&;RqP;NzPfT=&Og z8ILW^{PgXe50D4cj3_1)hKq0)EzS65{>u|*Y@9pg`Nr&)ERGjg+Qqni^ni=}yA=0j zlyF>>=Oo-yabK?R1HMT^|1vuN>FJNY-!%D$4?VEE?FaS8LMukUSu^9Yf6RLB>AcW2 zuRZs{SHJZ7l&9)ve(R;Jx%;2Hwq*Le2RAi*{F~yAt6uwL*-Kl0b^7hbM;<%+bnY7u zJ#b>)4TtU=bIWLDepo{O`G?{B4KVWCeSIH+8xG$E!QpU1!{57XiO&82LW| zyA+ZAyH7~wd?iz@@KV17&MLe^;SW-fap{t1zXc^bzgBpS!plUtF4pp{mOz=p?^E~} zu9U!Hh3g96p>V2mx=L}A{l`BglG>w^XqPJaxsxR@R^fXjPV9#qU2By5Bg&sz z32Q$ECHei+B=BQ}PgC;qRPa7j_}777qK(s<^CiIjY>txu>6H@yx{?oolKl_WxXI;h zQFyb;cb<|@D*QL9UV;k$s>0_e`CAqKw!&{v@?nMV5PnY5I#x@7`%_f;`GoS5(umgZ0r+T{IZff-BaR1jCWK$cBpgXryj!$+F6t>d4wqX=Oz6hUWy!wuFM^71g22 zihCkjeYB=7v>_6#sIG`=)irfxHJj6(W3}N>G{T(C6}9&BhDa11!I`qVf@K@;3YLc| zDz&OeRXuQJh#onEvW*c4)>c&8NUXZDrtB^|N%mOSskkvzpQcu}F;oqQotA`K<&~lO zjloD=s6G;`kCdTY^_s+BIT#9u>mv1xsH`P>b6rI=65L!<7gq8TDBX&3O9|npd=zmi zDqeD%5xi~17j;cOE)5hF>1z;gor@K~3L7KCaX#w;&aIrp6_TmowlL{j;;J}^p-=G7hd55pb${)g< zbBo`BE7yg}39r`IL&Q*EpXs!XcDXf2QKSMY8Vb&4XuK)+JU12?CT2-oW~?$ zu6N+hiEE<+_t{o7t=567p;d6VIPfbR@{JCByaR7>;4~-mwcUXaH@~sm2px&QNCZYA zFcN|Pn+Tlted;5lsXyDWyf5FZX~wp;XhyQrXgZpGL^zb3_ZHyf^j`e+O)J0?r8g2U z(UZhA{S8Xf6rSi5=~pOCQ(~e`q>odYro6;%kv>dmnz|BCi1YzU(^Q|>F49j?nx^tZ zqe%aR(llKsYDN0{l&0lJV!cRzhtf3VC02{{Hz`fK4v7Mh{yL>;`J2cW=?5rHQ+dKK z()UoBrr1P|NY_!CrnrPA(qEx8O`VB>bIGL9y7+DYMyr=_W8q+QB0~2tb_XbQqmw

Zft> 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 r3^(V`;|H Date: Fri, 9 Aug 2019 19:19:00 +0200 Subject: [PATCH 2/8] common/messages: Add SdProtect message. --- common/protob/messages-management.proto | 18 ++++++++++++ common/protob/messages.proto | 1 + core/src/trezor/messages/MessageType.py | 1 + core/src/trezor/messages/SdProtect.py | 28 +++++++++++++++++++ .../trezor/messages/SdProtectOperationType.py | 5 ++++ python/src/trezorlib/messages/MessageType.py | 1 + python/src/trezorlib/messages/SdProtect.py | 28 +++++++++++++++++++ .../messages/SdProtectOperationType.py | 5 ++++ python/src/trezorlib/messages/__init__.py | 2 ++ 9 files changed, 89 insertions(+) create mode 100644 core/src/trezor/messages/SdProtect.py create mode 100644 core/src/trezor/messages/SdProtectOperationType.py create mode 100644 python/src/trezorlib/messages/SdProtect.py create mode 100644 python/src/trezorlib/messages/SdProtectOperationType.py diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index 58c5dbd3d..83d99b057 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -131,6 +131,24 @@ message ChangePin { optional bool remove = 1; // is PIN removal requested? } +/** + * Request: Starts workflow for enabling/regenerating/disabling SD card protection + * @start + * @next Success + * @next Failure + */ +message SdProtect { + optional SdProtectOperationType operation = 1; + /** + * Structure representing SD card protection operation + */ + enum SdProtectOperationType { + DISABLE = 0; + ENABLE = 1; + REFRESH = 2; + } +} + /** * Request: Test if the device is alive, device sends back the message in Success response * @start diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 0ad3c5dae..4bfa3e941 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -61,6 +61,7 @@ enum MessageType { MessageType_WordAck = 47 [(wire_in) = true]; MessageType_GetFeatures = 55 [(wire_in) = true]; MessageType_SetU2FCounter = 63 [(wire_in) = true]; + MessageType_SdProtect = 79 [(wire_in) = true]; // Bootloader MessageType_FirmwareErase = 6 [(wire_in) = true, (wire_bootloader) = true]; diff --git a/core/src/trezor/messages/MessageType.py b/core/src/trezor/messages/MessageType.py index a4fb1eb4a..9c7223d7c 100644 --- a/core/src/trezor/messages/MessageType.py +++ b/core/src/trezor/messages/MessageType.py @@ -33,6 +33,7 @@ WordRequest = 46 WordAck = 47 GetFeatures = 55 SetU2FCounter = 63 +SdProtect = 79 FirmwareErase = 6 FirmwareUpload = 7 FirmwareRequest = 8 diff --git a/core/src/trezor/messages/SdProtect.py b/core/src/trezor/messages/SdProtect.py new file mode 100644 index 000000000..38fc0c62c --- /dev/null +++ b/core/src/trezor/messages/SdProtect.py @@ -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), + } diff --git a/core/src/trezor/messages/SdProtectOperationType.py b/core/src/trezor/messages/SdProtectOperationType.py new file mode 100644 index 000000000..c3960723b --- /dev/null +++ b/core/src/trezor/messages/SdProtectOperationType.py @@ -0,0 +1,5 @@ +# Automatically generated by pb2py +# fmt: off +DISABLE = 0 +ENABLE = 1 +REFRESH = 2 diff --git a/python/src/trezorlib/messages/MessageType.py b/python/src/trezorlib/messages/MessageType.py index fab7070f0..75f0ccfe6 100644 --- a/python/src/trezorlib/messages/MessageType.py +++ b/python/src/trezorlib/messages/MessageType.py @@ -31,6 +31,7 @@ WordRequest = 46 WordAck = 47 GetFeatures = 55 SetU2FCounter = 63 +SdProtect = 79 FirmwareErase = 6 FirmwareUpload = 7 FirmwareRequest = 8 diff --git a/python/src/trezorlib/messages/SdProtect.py b/python/src/trezorlib/messages/SdProtect.py new file mode 100644 index 000000000..48493f952 --- /dev/null +++ b/python/src/trezorlib/messages/SdProtect.py @@ -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), + } diff --git a/python/src/trezorlib/messages/SdProtectOperationType.py b/python/src/trezorlib/messages/SdProtectOperationType.py new file mode 100644 index 000000000..c3960723b --- /dev/null +++ b/python/src/trezorlib/messages/SdProtectOperationType.py @@ -0,0 +1,5 @@ +# Automatically generated by pb2py +# fmt: off +DISABLE = 0 +ENABLE = 1 +REFRESH = 2 diff --git a/python/src/trezorlib/messages/__init__.py b/python/src/trezorlib/messages/__init__.py index 1310b657b..5019c2bf5 100644 --- a/python/src/trezorlib/messages/__init__.py +++ b/python/src/trezorlib/messages/__init__.py @@ -201,6 +201,7 @@ from .RippleGetAddress import RippleGetAddress from .RipplePayment import RipplePayment from .RippleSignTx import RippleSignTx from .RippleSignedTx import RippleSignedTx +from .SdProtect import SdProtect from .SelfTest import SelfTest from .SetU2FCounter import SetU2FCounter from .SignIdentity import SignIdentity @@ -274,6 +275,7 @@ from . import PinMatrixRequestType from . import RecoveryDeviceType from . import RequestType from . import ResetDeviceBackupType +from . import SdProtectOperationType from . import TezosBallotType from . import TezosContractType from . import WordRequestType From f867b43251ff5ef7e6b3bdcd7b652be7db1a3d65 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Sun, 11 Aug 2019 20:52:19 +0200 Subject: [PATCH 3/8] trezorctl: Add sd-protect command. --- python/docs/OPTIONS.rst | 3 ++- python/src/trezorlib/cli/trezorctl.py | 32 ++++++++++++++++++++++++++- python/src/trezorlib/device.py | 6 +++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/python/docs/OPTIONS.rst b/python/docs/OPTIONS.rst index 54f2a388a..cb3a4dfd1 100644 --- a/python/docs/OPTIONS.rst +++ b/python/docs/OPTIONS.rst @@ -24,7 +24,7 @@ Use the following command to see all options: cardano-get-address Get Cardano address. cardano-get-public-key Get Cardano public key. cardano-sign-tx Sign Cardano transaction. - change-pin Change new PIN or remove existing. + change-pin Set, change or remove PIN. clear-session Clear session (remove cached PIN, passphrase, etc.). cosi-commit Ask device to commit to CoSi signing. cosi-sign Ask device to sign using CoSi. @@ -66,6 +66,7 @@ Use the following command to see all options: reset-device Perform device setup and generate new seed. ripple-get-address Get Ripple address ripple-sign-tx Sign Ripple transaction + sd-protect Secure the device with SD card protection. self-test Perform a self-test. set-auto-lock-delay Set auto-lock delay (in seconds). set-flags Set device flags. diff --git a/python/src/trezorlib/cli/trezorctl.py b/python/src/trezorlib/cli/trezorctl.py index a78893d3e..c23cd1582 100755 --- a/python/src/trezorlib/cli/trezorctl.py +++ b/python/src/trezorlib/cli/trezorctl.py @@ -123,6 +123,14 @@ CHOICE_RESET_DEVICE_TYPE = ChoiceType( } ) +CHOICE_SD_PROTECT_OPERATION_TYPE = ChoiceType( + { + "enable": proto.SdProtectOperationType.ENABLE, + "disable": proto.SdProtectOperationType.DISABLE, + "refresh": proto.SdProtectOperationType.REFRESH, + } +) + class UnderscoreAgnosticGroup(click.Group): """Command group that normalizes dashes and underscores. @@ -261,13 +269,35 @@ def get_features(connect): # -@cli.command(help="Change new PIN or remove existing.") +@cli.command(help="Set, change or remove PIN.") @click.option("-r", "--remove", is_flag=True) @click.pass_obj def change_pin(connect, remove): return device.change_pin(connect(), remove) +@cli.command() +@click.argument("operation", type=CHOICE_SD_PROTECT_OPERATION_TYPE) +@click.pass_obj +def sd_protect(connect, operation): + """Secure the device with SD card protection. + + When SD card protection is enabled, a randomly generated secret is stored + on the SD card. During every PIN checking and unlocking operation this + secret is combined with the entered PIN value to decrypt data stored on + the device. The SD card will thus be needed every time you unlock the + device. The options are: + + \b + enable - Generate SD card secret and use it to protect the PIN and storage. + disable - Remove SD card secret protection. + refresh - Replace the current SD card secret with a new one. + """ + if connect().features.model == "1": + raise click.BadUsage("Trezor One does not support SD card protection.") + return device.sd_protect(connect(), operation) + + @cli.command(help="Enable passphrase.") @click.pass_obj def enable_passphrase(connect): diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index 0e4355bb0..68cddfada 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -90,6 +90,12 @@ def change_pin(client, remove=False): return ret +@expect(proto.Success, field="message") +def sd_protect(client, operation): + ret = client.call(proto.SdProtect(operation=operation)) + return ret + + @expect(proto.Success, field="message") def set_u2f_counter(client, u2f_counter): ret = client.call(proto.SetU2FCounter(u2f_counter=u2f_counter)) From 6350b1c61c4209f704c5848346a4580d4e7c797a Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Tue, 13 Aug 2019 17:50:06 +0200 Subject: [PATCH 4/8] core: Implement SD card protection. --- .../extmod/modtrezorconfig/modtrezorconfig.c | 42 ++-- core/mocks/generated/trezorconfig.pyi | 8 +- core/src/apps/common/sd_salt.py | 193 ++++++++++++++++++ core/src/apps/common/storage/common.py | 4 +- core/src/apps/common/storage/device.py | 24 +++ core/src/apps/management/__init__.py | 1 + core/src/apps/management/change_pin.py | 40 ++-- core/src/apps/management/load_device.py | 2 +- .../management/recovery_device/__init__.py | 13 +- core/src/apps/management/reset_device.py | 2 +- core/src/apps/management/sd_protect.py | 169 +++++++++++++++ core/src/boot.py | 23 ++- core/tests/test_trezor.config.py | 60 ++++-- 13 files changed, 515 insertions(+), 66 deletions(-) create mode 100644 core/src/apps/common/sd_salt.py create mode 100644 core/src/apps/management/sd_protect.py diff --git a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c index 3c6356883..b8755a409 100644 --- a/core/embed/extmod/modtrezorconfig/modtrezorconfig.c +++ b/core/embed/extmod/modtrezorconfig/modtrezorconfig.c @@ -67,41 +67,39 @@ STATIC mp_obj_t mod_trezorconfig_init(size_t n_args, const mp_obj_t *args) { STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_init_obj, 0, 1, mod_trezorconfig_init); -/// def unlock(pin: int, ext_salt: Optional[bytes] = None) -> bool: +/// def unlock(pin: int, ext_salt: Optional[bytes]) -> bool: /// """ /// Attempts to unlock the storage with the given PIN and external salt. /// Returns True on success, False on failure. /// """ -STATIC mp_obj_t mod_trezorconfig_unlock(size_t n_args, const mp_obj_t *args) { - uint32_t pin = trezor_obj_get_uint(args[0]); - const uint8_t *ext_salt = NULL; - if (n_args > 1 && args[1] != mp_const_none) { - mp_buffer_info_t ext_salt_b; - mp_get_buffer_raise(args[1], &ext_salt_b, MP_BUFFER_READ); +STATIC mp_obj_t mod_trezorconfig_unlock(mp_obj_t pin, mp_obj_t ext_salt) { + uint32_t pin_i = trezor_obj_get_uint(pin); + mp_buffer_info_t ext_salt_b; + ext_salt_b.buf = NULL; + if (ext_salt != mp_const_none) { + mp_get_buffer_raise(ext_salt, &ext_salt_b, MP_BUFFER_READ); if (ext_salt_b.len != EXTERNAL_SALT_SIZE) mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt."); - ext_salt = ext_salt_b.buf; } - if (sectrue != storage_unlock(pin, ext_salt)) { + if (sectrue != storage_unlock(pin_i, ext_salt_b.buf)) { return mp_const_false; } return mp_const_true; } -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_unlock_obj, 1, 2, - mod_trezorconfig_unlock); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorconfig_unlock_obj, + mod_trezorconfig_unlock); -/// def check_pin(pin: int, ext_salt: Optional[bytes] = None) -> bool: +/// def check_pin(pin: int, ext_salt: Optional[bytes]) -> bool: /// """ /// Check the given PIN with the given external salt. /// Returns True on success, False on failure. /// """ -STATIC mp_obj_t mod_trezorconfig_check_pin(size_t n_args, - const mp_obj_t *args) { - return mod_trezorconfig_unlock(n_args, args); +STATIC mp_obj_t mod_trezorconfig_check_pin(mp_obj_t pin, mp_obj_t ext_salt) { + return mod_trezorconfig_unlock(pin, ext_salt); } -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_check_pin_obj, 1, 2, - mod_trezorconfig_check_pin); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorconfig_check_pin_obj, + mod_trezorconfig_check_pin); /// def lock() -> None: /// """ @@ -140,8 +138,8 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorconfig_get_pin_rem_obj, /// def change_pin( /// oldpin: int, /// newpin: int, -/// old_ext_salt: Optional[bytes] = None, -/// new_ext_salt: Optional[bytes] = None, +/// old_ext_salt: Optional[bytes], +/// new_ext_salt: Optional[bytes], /// ) -> bool: /// """ /// Change PIN and external salt. Returns True on success, False on failure. @@ -152,14 +150,14 @@ STATIC mp_obj_t mod_trezorconfig_change_pin(size_t n_args, uint32_t newpin = trezor_obj_get_uint(args[1]); mp_buffer_info_t ext_salt_b; const uint8_t *old_ext_salt = NULL; - if (n_args > 2 && args[2] != mp_const_none) { + if (args[2] != mp_const_none) { mp_get_buffer_raise(args[2], &ext_salt_b, MP_BUFFER_READ); if (ext_salt_b.len != EXTERNAL_SALT_SIZE) mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt."); old_ext_salt = ext_salt_b.buf; } const uint8_t *new_ext_salt = NULL; - if (n_args > 3 && args[3] != mp_const_none) { + if (args[3] != mp_const_none) { mp_get_buffer_raise(args[3], &ext_salt_b, MP_BUFFER_READ); if (ext_salt_b.len != EXTERNAL_SALT_SIZE) mp_raise_msg(&mp_type_ValueError, "Invalid length of external salt."); @@ -172,7 +170,7 @@ STATIC mp_obj_t mod_trezorconfig_change_pin(size_t n_args, } return mp_const_true; } -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_change_pin_obj, 2, +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorconfig_change_pin_obj, 4, 4, mod_trezorconfig_change_pin); /// def get(app: int, key: int, public: bool = False) -> Optional[bytes]: diff --git a/core/mocks/generated/trezorconfig.pyi b/core/mocks/generated/trezorconfig.pyi index ae3742467..5b8645b9b 100644 --- a/core/mocks/generated/trezorconfig.pyi +++ b/core/mocks/generated/trezorconfig.pyi @@ -12,7 +12,7 @@ def init( # extmod/modtrezorconfig/modtrezorconfig.c -def unlock(pin: int, ext_salt: Optional[bytes] = None) -> bool: +def unlock(pin: int, ext_salt: Optional[bytes]) -> bool: """ Attempts to unlock the storage with the given PIN and external salt. Returns True on success, False on failure. @@ -20,7 +20,7 @@ def unlock(pin: int, ext_salt: Optional[bytes] = None) -> bool: # extmod/modtrezorconfig/modtrezorconfig.c -def check_pin(pin: int, ext_salt: Optional[bytes] = None) -> bool: +def check_pin(pin: int, ext_salt: Optional[bytes]) -> bool: """ Check the given PIN with the given external salt. Returns True on success, False on failure. @@ -52,8 +52,8 @@ def get_pin_rem() -> int: def change_pin( oldpin: int, newpin: int, - old_ext_salt: Optional[bytes] = None, - new_ext_salt: Optional[bytes] = None, + old_ext_salt: Optional[bytes], + new_ext_salt: Optional[bytes], ) -> bool: """ Change PIN and external salt. Returns True on success, False on failure. diff --git a/core/src/apps/common/sd_salt.py b/core/src/apps/common/sd_salt.py new file mode 100644 index 000000000..94b81fa81 --- /dev/null +++ b/core/src/apps/common/sd_salt.py @@ -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) diff --git a/core/src/apps/common/storage/common.py b/core/src/apps/common/storage/common.py index af188f118..f00c3ea36 100644 --- a/core/src/apps/common/storage/common.py +++ b/core/src/apps/common/storage/common.py @@ -28,8 +28,8 @@ def get(app: int, key: int, public: bool = False) -> Optional[bytes]: return config.get(app, key, public) -def delete(app: int, key: int) -> None: - config.delete(app, key) +def delete(app: int, key: int, public: bool = False) -> None: + config.delete(app, key, public) def set_true_or_delete(app: int, key: int, value: bool) -> None: diff --git a/core/src/apps/common/storage/device.py b/core/src/apps/common/storage/device.py index 693e82579..a9104db86 100644 --- a/core/src/apps/common/storage/device.py +++ b/core/src/apps/common/storage/device.py @@ -3,6 +3,7 @@ from ubinascii import hexlify from trezor.crypto import random +from apps.common.sd_salt import SD_SALT_AUTH_KEY_LEN_BYTES from apps.common.storage import common if False: @@ -31,6 +32,7 @@ _MNEMONIC_TYPE = const(0x0E) # int _ROTATION = const(0x0F) # int _SLIP39_IDENTIFIER = const(0x10) # bool _SLIP39_ITERATION_EXPONENT = const(0x11) # int +_SD_SALT_AUTH_KEY = const(0x12) # bytes # fmt: on HOMESCREEN_MAXSIZE = 16384 @@ -234,3 +236,25 @@ def get_slip39_iteration_exponent() -> Optional[int]: The device's actual SLIP-39 iteration exponent used in passphrase derivation. """ return common.get_uint8(_NAMESPACE, _SLIP39_ITERATION_EXPONENT) + + +def get_sd_salt_auth_key() -> Optional[bytes]: + """ + The key used to check the authenticity of the SD card salt. + """ + auth_key = common.get(_NAMESPACE, _SD_SALT_AUTH_KEY, public=True) + if auth_key is not None and len(auth_key) != SD_SALT_AUTH_KEY_LEN_BYTES: + raise ValueError + return auth_key + + +def set_sd_salt_auth_key(auth_key: Optional[bytes]) -> None: + """ + The key used to check the authenticity of the SD card salt. + """ + if auth_key is not None: + if len(auth_key) != SD_SALT_AUTH_KEY_LEN_BYTES: + raise ValueError + return common.set(_NAMESPACE, _SD_SALT_AUTH_KEY, auth_key, public=True) + else: + return common.delete(_NAMESPACE, _SD_SALT_AUTH_KEY, public=True) diff --git a/core/src/apps/management/__init__.py b/core/src/apps/management/__init__.py index e1bbbc386..51eb80ced 100644 --- a/core/src/apps/management/__init__.py +++ b/core/src/apps/management/__init__.py @@ -14,3 +14,4 @@ def boot() -> None: wire.add(MessageType.ApplyFlags, __name__, "apply_flags") wire.add(MessageType.ChangePin, __name__, "change_pin") wire.add(MessageType.SetU2FCounter, __name__, "set_u2f_counter") + wire.add(MessageType.SdProtect, __name__, "sd_protect") diff --git a/core/src/apps/management/change_pin.py b/core/src/apps/management/change_pin.py index 793eef62a..b8dbdf61a 100644 --- a/core/src/apps/management/change_pin.py +++ b/core/src/apps/management/change_pin.py @@ -9,26 +9,25 @@ from trezor.ui.text import Text from apps.common.confirm import require_confirm from apps.common.request_pin import PinCancelled, request_pin +from apps.common.sd_salt import request_sd_salt +from apps.common.storage import device if False: - from typing import Any + from typing import Any, Optional, Tuple from trezor.messages.ChangePin import ChangePin async def change_pin(ctx: wire.Context, msg: ChangePin) -> Success: - # confirm that user wants to change the pin await require_confirm_change_pin(ctx, msg) - # get current pin, return failure if invalid - if config.has_pin(): - curpin = await request_pin_ack(ctx, "Enter old PIN", config.get_pin_rem()) - # if removing, defer check to change_pin() - if not msg.remove: - if not config.check_pin(pin_to_int(curpin)): - raise wire.PinInvalid("PIN invalid") - else: - curpin = "" + # get old pin + curpin, salt = await request_pin_and_sd_salt(ctx, "Enter old PIN") + + # if changing pin, pre-check the entered pin before getting new pin + if curpin and not msg.remove: + if not config.check_pin(pin_to_int(curpin), salt): + raise wire.PinInvalid("PIN invalid") # get new pin if not msg.remove: @@ -37,7 +36,7 @@ async def change_pin(ctx: wire.Context, msg: ChangePin) -> Success: newpin = "" # write into storage - if not config.change_pin(pin_to_int(curpin), pin_to_int(newpin)): + if not config.change_pin(pin_to_int(curpin), pin_to_int(newpin), salt, salt): raise wire.PinInvalid("PIN invalid") if newpin: @@ -77,6 +76,23 @@ async def request_pin_confirm(ctx: wire.Context, *args: Any, **kwargs: Any) -> s await pin_mismatch() +async def request_pin_and_sd_salt( + ctx: wire.Context, prompt: str = "Enter your PIN", allow_cancel: bool = True +) -> Tuple[str, Optional[bytearray]]: + salt_auth_key = device.get_sd_salt_auth_key() + if salt_auth_key is not None: + salt = await request_sd_salt(ctx, salt_auth_key) # type: Optional[bytearray] + else: + salt = None + + if config.has_pin(): + pin = await request_pin_ack(ctx, prompt, config.get_pin_rem(), allow_cancel) + else: + pin = "" + + return pin, salt + + async def request_pin_ack(ctx: wire.Context, *args: Any, **kwargs: Any) -> str: try: await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck) diff --git a/core/src/apps/management/load_device.py b/core/src/apps/management/load_device.py index 7fabba906..b7b8aeeb7 100644 --- a/core/src/apps/management/load_device.py +++ b/core/src/apps/management/load_device.py @@ -55,6 +55,6 @@ async def load_device(ctx, msg): use_passphrase=msg.passphrase_protection, label=msg.label ) if msg.pin: - config.change_pin(pin_to_int(""), pin_to_int(msg.pin)) + config.change_pin(pin_to_int(""), pin_to_int(msg.pin), None, None) return Success(message="Device loaded") diff --git a/core/src/apps/management/recovery_device/__init__.py b/core/src/apps/management/recovery_device/__init__.py index fa13558ed..8342c5b23 100644 --- a/core/src/apps/management/recovery_device/__init__.py +++ b/core/src/apps/management/recovery_device/__init__.py @@ -6,7 +6,7 @@ from trezor.ui.text import Text from apps.common import storage from apps.common.confirm import require_confirm -from apps.management.change_pin import request_pin_ack, request_pin_confirm +from apps.management.change_pin import request_pin_and_sd_salt, request_pin_confirm from apps.management.recovery_device.homescreen import recovery_process if False: @@ -24,13 +24,10 @@ async def recovery_device(ctx: wire.Context, msg: RecoveryDevice) -> Success: await _continue_dialog(ctx, msg) - # for dry run pin needs to entered + # for dry run pin needs to be entered if msg.dry_run: - if config.has_pin(): - curpin = await request_pin_ack(ctx, "Enter PIN", config.get_pin_rem()) - else: - curpin = "" - if not config.check_pin(pin_to_int(curpin)): + curpin, salt = await request_pin_and_sd_salt(ctx, "Enter PIN") + if not config.check_pin(pin_to_int(curpin), salt): raise wire.PinInvalid("PIN invalid") # set up pin if requested @@ -38,7 +35,7 @@ async def recovery_device(ctx: wire.Context, msg: RecoveryDevice) -> Success: if msg.dry_run: raise wire.ProcessError("Can't setup PIN during dry_run recovery.") newpin = await request_pin_confirm(ctx, allow_cancel=False) - config.change_pin(pin_to_int(""), pin_to_int(newpin)) + config.change_pin(pin_to_int(""), pin_to_int(newpin), None, None) if msg.u2f_counter: storage.device.set_u2f_counter(msg.u2f_counter) diff --git a/core/src/apps/management/reset_device.py b/core/src/apps/management/reset_device.py index 7770b7234..1c96c0949 100644 --- a/core/src/apps/management/reset_device.py +++ b/core/src/apps/management/reset_device.py @@ -71,7 +71,7 @@ async def reset_device(ctx: wire.Context, msg: ResetDevice) -> Success: await backup_bip39_wallet(ctx, secret) # write PIN into storage - if not config.change_pin(pin_to_int(""), pin_to_int(newpin)): + if not config.change_pin(pin_to_int(""), pin_to_int(newpin), None, None): raise wire.ProcessError("Could not change PIN") # write settings and master secret into storage diff --git a/core/src/apps/management/sd_protect.py b/core/src/apps/management/sd_protect.py new file mode 100644 index 000000000..fe9c2da36 --- /dev/null +++ b/core/src/apps/management/sd_protect.py @@ -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.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 +from apps.management.change_pin import request_pin_ack, request_pin_and_sd_salt + +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) diff --git a/core/src/boot.py b/core/src/boot.py index f6c414482..cbcf31065 100644 --- a/core/src/boot.py +++ b/core/src/boot.py @@ -3,21 +3,38 @@ from trezor.pin import pin_to_int, show_pin_timeout from apps.common import storage from apps.common.request_pin import request_pin +from apps.common.sd_salt import request_sd_salt +from apps.common.storage import device + +if False: + from typing import Optional async def bootscreen() -> None: ui.display.orientation(storage.device.get_rotation()) + salt_auth_key = device.get_sd_salt_auth_key() + while True: try: + if salt_auth_key is not None or config.has_pin(): + await lockscreen() + + if salt_auth_key is not None: + salt = await request_sd_salt( + None, salt_auth_key + ) # type: Optional[bytearray] + else: + salt = None + if not config.has_pin(): - config.unlock(pin_to_int("")) + config.unlock(pin_to_int(""), salt) storage.init_unlocked() return - await lockscreen() + label = "Enter your PIN" while True: pin = await request_pin(label, config.get_pin_rem()) - if config.unlock(pin_to_int(pin)): + if config.unlock(pin_to_int(pin), salt): storage.init_unlocked() return else: diff --git a/core/tests/test_trezor.config.py b/core/tests/test_trezor.config.py index 416ec3059..a6879996d 100644 --- a/core/tests/test_trezor.config.py +++ b/core/tests/test_trezor.config.py @@ -27,7 +27,7 @@ class TestConfig(unittest.TestCase): def test_wipe(self): config.init() config.wipe() - self.assertEqual(config.unlock(pin_to_int('')), True) + self.assertEqual(config.unlock(pin_to_int(''), None), True) config.set(1, 1, b'hello') config.set(1, 2, b'world') v0 = config.get(1, 1) @@ -44,7 +44,7 @@ class TestConfig(unittest.TestCase): for _ in range(128): config.init() config.wipe() - self.assertEqual(config.unlock(pin_to_int('')), True) + self.assertEqual(config.unlock(pin_to_int(''), None), True) appid, key = random_entry() value = random.bytes(16) config.set(appid, key, value) @@ -58,7 +58,7 @@ class TestConfig(unittest.TestCase): def test_public(self): config.init() config.wipe() - self.assertEqual(config.unlock(pin_to_int('')), True) + self.assertEqual(config.unlock(pin_to_int(''), None), True) appid, key = random_entry() @@ -84,25 +84,59 @@ class TestConfig(unittest.TestCase): def test_change_pin(self): config.init() config.wipe() - self.assertEqual(config.unlock(pin_to_int('')), True) + self.assertEqual(config.unlock(pin_to_int(''), None), True) with self.assertRaises(RuntimeError): config.set(PINAPP, PINKEY, b'value') - self.assertEqual(config.change_pin(pin_to_int('000'), pin_to_int('666')), False) - self.assertEqual(config.change_pin(pin_to_int(''), pin_to_int('000')), True) + self.assertEqual(config.change_pin(pin_to_int('000'), pin_to_int('666'), None, None), False) + self.assertEqual(config.change_pin(pin_to_int(''), pin_to_int('000'), None, None), True) self.assertEqual(config.get(PINAPP, PINKEY), None) config.set(1, 1, b'value') config.init() - self.assertEqual(config.unlock(pin_to_int('000')), True) - config.change_pin(pin_to_int('000'), pin_to_int('')) + self.assertEqual(config.unlock(pin_to_int('000'), None), True) + config.change_pin(pin_to_int('000'), pin_to_int(''), None, None) config.init() - self.assertEqual(config.unlock(pin_to_int('000')), False) - self.assertEqual(config.unlock(pin_to_int('')), True) + self.assertEqual(config.unlock(pin_to_int('000'), None), False) + self.assertEqual(config.unlock(pin_to_int(''), None), True) + self.assertEqual(config.get(1, 1), b'value') + + def test_change_sd_salt(self): + salt1 = b"0123456789abcdef0123456789abcdef" + salt2 = b"0123456789ABCDEF0123456789ABCDEF" + + # Enable PIN and SD salt. + config.init() + config.wipe() + self.assertTrue(config.unlock(pin_to_int(''), None)) + config.set(1, 1, b'value') + self.assertFalse(config.change_pin(pin_to_int(''), pin_to_int(''), salt1, None)) + self.assertTrue(config.change_pin(pin_to_int(''), pin_to_int('000'), None, salt1)) + self.assertEqual(config.get(1, 1), b'value') + + # Disable PIN and change SD salt. + config.init() + self.assertFalse(config.unlock(pin_to_int('000'), None)) + self.assertIsNone(config.get(1, 1)) + self.assertTrue(config.unlock(pin_to_int('000'), salt1)) + self.assertTrue(config.change_pin(pin_to_int('000'), pin_to_int(''), salt1, salt2)) + self.assertEqual(config.get(1, 1), b'value') + + # Disable SD salt. + config.init() + self.assertFalse(config.unlock(pin_to_int('000'), salt2)) + self.assertIsNone(config.get(1, 1)) + self.assertTrue(config.unlock(pin_to_int(''), salt2)) + self.assertTrue(config.change_pin(pin_to_int(''), pin_to_int(''), salt2, None)) + self.assertEqual(config.get(1, 1), b'value') + + # Check that PIN and SD salt are disabled. + config.init() + self.assertTrue(config.unlock(pin_to_int(''), None)) self.assertEqual(config.get(1, 1), b'value') def test_set_get(self): config.init() config.wipe() - self.assertEqual(config.unlock(pin_to_int('')), True) + self.assertEqual(config.unlock(pin_to_int(''), None), True) for _ in range(32): appid, key = random_entry() value = random.bytes(128) @@ -113,7 +147,7 @@ class TestConfig(unittest.TestCase): def test_compact(self): config.init() config.wipe() - self.assertEqual(config.unlock(pin_to_int('')), True) + self.assertEqual(config.unlock(pin_to_int(''), None), True) appid, key = 1, 1 for _ in range(259): value = random.bytes(259) @@ -124,7 +158,7 @@ class TestConfig(unittest.TestCase): def test_get_default(self): config.init() config.wipe() - self.assertEqual(config.unlock(pin_to_int('')), True) + self.assertEqual(config.unlock(pin_to_int(''), None), True) for _ in range(128): appid, key = random_entry() value = config.get(appid, key) From 262434ea1ba30d46f8c877af064298358cfd67c5 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Sun, 1 Sep 2019 20:12:49 +0200 Subject: [PATCH 5/8] python/tests: Add SD protection device test. --- core/src/boot.py | 15 +++++- tests/device_tests/test_msg_sd_protect.py | 62 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests/device_tests/test_msg_sd_protect.py diff --git a/core/src/boot.py b/core/src/boot.py index cbcf31065..a9f48d6ad 100644 --- a/core/src/boot.py +++ b/core/src/boot.py @@ -1,4 +1,4 @@ -from trezor import config, log, loop, res, ui +from trezor import config, io, log, loop, res, ui, utils from trezor.pin import pin_to_int, show_pin_timeout from apps.common import storage @@ -72,6 +72,19 @@ async def lockscreen() -> None: await ui.click() +if utils.EMULATOR: + # Ensure the emulated SD card is FAT32 formatted. + sd = io.SDCard() + fs = io.FatFS() + sd.power(True) + try: + fs.mount() + except OSError: + fs.mkfs() + else: + fs.unmount() + sd.power(False) + ui.display.backlight(ui.BACKLIGHT_NONE) ui.backlight_fade(ui.BACKLIGHT_NORMAL) config.init(show_pin_timeout) diff --git a/tests/device_tests/test_msg_sd_protect.py b/tests/device_tests/test_msg_sd_protect.py new file mode 100644 index 000000000..943d968fc --- /dev/null +++ b/tests/device_tests/test_msg_sd_protect.py @@ -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 . + +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) From 3c20b1b1cd14cfb6abe163aa549a4cc6a750f793 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Thu, 5 Sep 2019 17:13:31 +0200 Subject: [PATCH 6/8] legacy: Add SdProtect to SKIPPED_MESSAGES --- legacy/firmware/protob/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/firmware/protob/Makefile b/legacy/firmware/protob/Makefile index 1608b327a..52f91a085 100644 --- a/legacy/firmware/protob/Makefile +++ b/legacy/firmware/protob/Makefile @@ -2,7 +2,7 @@ ifneq ($(V),1) Q := @ endif -SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple Tezos WebAuthn +SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdProtect Tezos WebAuthn ifeq ($(BITCOIN_ONLY), 1) SKIPPED_MESSAGES += Ethereum Lisk NEM Stellar From f5ea81f9050297f6f5042c2fee83ad03b161aa1f Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Mon, 16 Sep 2019 23:08:42 +0200 Subject: [PATCH 7/8] core: Move some PIN-related functions to apps.common.request_pin and implement verify_user_pin(). --- core/src/apps/common/request_pin.py | 83 ++++++++++++++++++- core/src/apps/management/change_pin.py | 52 +----------- .../management/recovery_device/__init__.py | 2 +- core/src/apps/management/reset_device.py | 2 +- core/src/apps/management/sd_protect.py | 2 +- core/src/apps/webauthn/__init__.py | 19 ++--- core/src/trezor/ui/pin.py | 10 ++- 7 files changed, 100 insertions(+), 70 deletions(-) diff --git a/core/src/apps/common/request_pin.py b/core/src/apps/common/request_pin.py index 7c6ae4cb6..999d1dddc 100644 --- a/core/src/apps/common/request_pin.py +++ b/core/src/apps/common/request_pin.py @@ -1,5 +1,17 @@ -from trezor import loop +from trezor import config, loop, ui, wire +from trezor.messages import ButtonRequestType +from trezor.messages.ButtonAck import ButtonAck +from trezor.messages.ButtonRequest import ButtonRequest +from trezor.pin import pin_to_int from trezor.ui.pin import CANCELLED, PinDialog +from trezor.ui.popup import Popup +from trezor.ui.text import Text + +from apps.common.sd_salt import request_sd_salt +from apps.common.storage import device + +if False: + from typing import Any, Optional, Tuple if __debug__: from apps.debug import input_signal @@ -9,6 +21,10 @@ class PinCancelled(Exception): pass +class PinInvalid(Exception): + pass + + async def request_pin( prompt: str = "Enter your PIN", attempts_remaining: int = None, @@ -31,3 +47,68 @@ async def request_pin( if result is CANCELLED: raise PinCancelled return result + + +async def request_pin_ack(ctx: wire.Context, *args: Any, **kwargs: Any) -> str: + try: + await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck) + return await ctx.wait(request_pin(*args, **kwargs)) + except PinCancelled: + raise wire.ActionCancelled("Cancelled") + + +async def request_pin_confirm(ctx: wire.Context, *args: Any, **kwargs: Any) -> str: + while True: + pin1 = await request_pin_ack(ctx, "Enter new PIN", *args, **kwargs) + pin2 = await request_pin_ack(ctx, "Re-enter new PIN", *args, **kwargs) + if pin1 == pin2: + return pin1 + await pin_mismatch() + + +async def pin_mismatch() -> None: + text = Text("PIN mismatch", ui.ICON_WRONG, ui.RED) + text.normal("The PINs you entered", "do not match.") + text.normal("") + text.normal("Please try again.") + popup = Popup(text, 3000) # show for 3 seconds + await popup + + +async def request_pin_and_sd_salt( + ctx: wire.Context, prompt: str = "Enter your PIN", allow_cancel: bool = True +) -> Tuple[str, Optional[bytearray]]: + salt_auth_key = device.get_sd_salt_auth_key() + if salt_auth_key is not None: + salt = await request_sd_salt(ctx, salt_auth_key) # type: Optional[bytearray] + else: + salt = None + + if config.has_pin(): + pin = await request_pin_ack(ctx, prompt, config.get_pin_rem(), allow_cancel) + else: + pin = "" + + return pin, salt + + +async def verify_user_pin( + prompt: str = "Enter your PIN", allow_cancel: bool = True, retry: bool = True +) -> None: + salt_auth_key = device.get_sd_salt_auth_key() + if salt_auth_key is not None: + salt = await request_sd_salt(None, salt_auth_key) # type: Optional[bytearray] + else: + salt = None + + if not config.has_pin() and not config.check_pin(pin_to_int(""), salt): + raise RuntimeError + + while retry: + pin = await request_pin(prompt, config.get_pin_rem(), allow_cancel) + if config.check_pin(pin_to_int(pin), salt): + return + else: + prompt = "Wrong PIN, enter again" + + raise PinInvalid diff --git a/core/src/apps/management/change_pin.py b/core/src/apps/management/change_pin.py index b8dbdf61a..90d0a533e 100644 --- a/core/src/apps/management/change_pin.py +++ b/core/src/apps/management/change_pin.py @@ -1,19 +1,12 @@ from trezor import config, ui, wire -from trezor.messages import ButtonRequestType -from trezor.messages.ButtonAck import ButtonAck -from trezor.messages.ButtonRequest import ButtonRequest from trezor.messages.Success import Success from trezor.pin import pin_to_int -from trezor.ui.popup import Popup from trezor.ui.text import Text from apps.common.confirm import require_confirm -from apps.common.request_pin import PinCancelled, request_pin -from apps.common.sd_salt import request_sd_salt -from apps.common.storage import device +from apps.common.request_pin import request_pin_and_sd_salt, request_pin_confirm if False: - from typing import Any, Optional, Tuple from trezor.messages.ChangePin import ChangePin @@ -65,46 +58,3 @@ def require_confirm_change_pin(ctx: wire.Context, msg: ChangePin) -> None: text.normal("Do you really want to") text.bold("enable PIN protection?") return require_confirm(ctx, text) - - -async def request_pin_confirm(ctx: wire.Context, *args: Any, **kwargs: Any) -> str: - while True: - pin1 = await request_pin_ack(ctx, "Enter new PIN", *args, **kwargs) - pin2 = await request_pin_ack(ctx, "Re-enter new PIN", *args, **kwargs) - if pin1 == pin2: - return pin1 - await pin_mismatch() - - -async def request_pin_and_sd_salt( - ctx: wire.Context, prompt: str = "Enter your PIN", allow_cancel: bool = True -) -> Tuple[str, Optional[bytearray]]: - salt_auth_key = device.get_sd_salt_auth_key() - if salt_auth_key is not None: - salt = await request_sd_salt(ctx, salt_auth_key) # type: Optional[bytearray] - else: - salt = None - - if config.has_pin(): - pin = await request_pin_ack(ctx, prompt, config.get_pin_rem(), allow_cancel) - else: - pin = "" - - return pin, salt - - -async def request_pin_ack(ctx: wire.Context, *args: Any, **kwargs: Any) -> str: - try: - await ctx.call(ButtonRequest(code=ButtonRequestType.Other), ButtonAck) - return await ctx.wait(request_pin(*args, **kwargs)) - except PinCancelled: - raise wire.ActionCancelled("Cancelled") - - -async def pin_mismatch() -> None: - text = Text("PIN mismatch", ui.ICON_WRONG, ui.RED) - text.normal("The PINs you entered", "do not match.") - text.normal("") - text.normal("Please try again.") - popup = Popup(text, 3000) # show for 3 seconds - await popup diff --git a/core/src/apps/management/recovery_device/__init__.py b/core/src/apps/management/recovery_device/__init__.py index 8342c5b23..09b3e1817 100644 --- a/core/src/apps/management/recovery_device/__init__.py +++ b/core/src/apps/management/recovery_device/__init__.py @@ -6,7 +6,7 @@ from trezor.ui.text import Text from apps.common import storage from apps.common.confirm import require_confirm -from apps.management.change_pin import request_pin_and_sd_salt, request_pin_confirm +from apps.common.request_pin import request_pin_and_sd_salt, request_pin_confirm from apps.management.recovery_device.homescreen import recovery_process if False: diff --git a/core/src/apps/management/reset_device.py b/core/src/apps/management/reset_device.py index 1c96c0949..5c5d05930 100644 --- a/core/src/apps/management/reset_device.py +++ b/core/src/apps/management/reset_device.py @@ -10,7 +10,7 @@ from trezor.ui.text import Text from apps.common import mnemonic, storage from apps.common.confirm import require_confirm -from apps.management.change_pin import request_pin_confirm +from apps.common.request_pin import request_pin_confirm from apps.management.common import layout if __debug__: diff --git a/core/src/apps/management/sd_protect.py b/core/src/apps/management/sd_protect.py index fe9c2da36..4d980ed9b 100644 --- a/core/src/apps/management/sd_protect.py +++ b/core/src/apps/management/sd_protect.py @@ -7,6 +7,7 @@ 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, @@ -17,7 +18,6 @@ from apps.common.sd_salt import ( stage_sd_salt, ) from apps.common.storage import device, is_initialized -from apps.management.change_pin import request_pin_ack, request_pin_and_sd_salt if False: from trezor.messages.SdProtect import SdProtect diff --git a/core/src/apps/webauthn/__init__.py b/core/src/apps/webauthn/__init__.py index 71a7efff0..ce66aa61b 100644 --- a/core/src/apps/webauthn/__init__.py +++ b/core/src/apps/webauthn/__init__.py @@ -512,20 +512,15 @@ class KeepaliveCallback: send_cmd_sync(cmd_keepalive(self.cid, _KEEPALIVE_STATUS_PROCESSING), self.iface) -async def check_pin(keepalive_callback: KeepaliveCallback) -> bool: - from apps.common.request_pin import PinCancelled, request_pin +async def verify_user(keepalive_callback: KeepaliveCallback) -> bool: + from apps.common.request_pin import verify_user_pin, PinCancelled, PinInvalid import trezor.pin try: trezor.pin.keepalive_callback = keepalive_callback - if config.has_pin(): - pin = await request_pin("Enter your PIN", config.get_pin_rem()) - while config.unlock(trezor.pin.pin_to_int(pin)) is not True: - pin = await request_pin("Wrong PIN, enter again", config.get_pin_rem()) - ret = True - else: - ret = config.unlock(trezor.pin.pin_to_int("")) - except PinCancelled: + await verify_user_pin() + ret = True + except (PinCancelled, PinInvalid): ret = False finally: trezor.pin.keepalive_callback = None @@ -695,7 +690,7 @@ class Fido2ConfirmMakeCredential(Fido2State, ConfirmInfo): if not await confirm(content): return False if self._user_verification: - return await check_pin(KeepaliveCallback(self.cid, self.iface)) + return await verify_user(KeepaliveCallback(self.cid, self.iface)) return True async def on_confirm(self) -> None: @@ -764,7 +759,7 @@ class Fido2ConfirmGetAssertion(Fido2State, ConfirmInfo, Pageable): if await ConfirmPageable(self, content) is not CONFIRMED: return False if self._user_verification: - return await check_pin(KeepaliveCallback(self.cid, self.iface)) + return await verify_user(KeepaliveCallback(self.cid, self.iface)) return True async def on_confirm(self) -> None: diff --git a/core/src/trezor/ui/pin.py b/core/src/trezor/ui/pin.py index a69cc0526..d25b1136b 100644 --- a/core/src/trezor/ui/pin.py +++ b/core/src/trezor/ui/pin.py @@ -12,7 +12,7 @@ from trezor.ui.button import ( ) if False: - from typing import Iterable + from typing import Iterable, Optional def digit_area(i: int) -> ui.Area: @@ -30,7 +30,7 @@ def generate_digits() -> Iterable[int]: class PinInput(ui.Component): - def __init__(self, prompt: str, subprompt: str, pin: str) -> None: + def __init__(self, prompt: str, subprompt: Optional[str], pin: str) -> None: self.prompt = prompt self.subprompt = subprompt self.pin = pin @@ -82,7 +82,11 @@ CANCELLED = object() class PinDialog(ui.Layout): def __init__( - self, prompt: str, subprompt: str, allow_cancel: bool = True, maxlength: int = 9 + self, + prompt: str, + subprompt: Optional[str], + allow_cancel: bool = True, + maxlength: int = 9, ) -> None: self.maxlength = maxlength self.input = PinInput(prompt, subprompt, "") From a5f5a1709f5800d3b92eb462db2410749a67db38 Mon Sep 17 00:00:00 2001 From: Andrew Kozlik Date: Wed, 18 Sep 2019 17:08:49 +0200 Subject: [PATCH 8/8] core/tests: Increase sleep to 30 after emulator launch to allow for FatFS.mkfs. --- core/tests/run_tests_device_emu.sh | 2 +- core/tests/run_tests_device_emu_monero.sh | 2 +- core/tests/run_tests_device_emu_u2f.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/tests/run_tests_device_emu.sh b/core/tests/run_tests_device_emu.sh index f214a1b63..e810d73fd 100755 --- a/core/tests/run_tests_device_emu.sh +++ b/core/tests/run_tests_device_emu.sh @@ -25,7 +25,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then $MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" & upy_pid=$! cd - - sleep 1 + sleep 30 fi # run tests diff --git a/core/tests/run_tests_device_emu_monero.sh b/core/tests/run_tests_device_emu_monero.sh index feb8d67eb..714dd3bb6 100755 --- a/core/tests/run_tests_device_emu_monero.sh +++ b/core/tests/run_tests_device_emu_monero.sh @@ -27,7 +27,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then $MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" & upy_pid=$! cd - - sleep 1 + sleep 30 fi DOCKER_ID="" diff --git a/core/tests/run_tests_device_emu_u2f.sh b/core/tests/run_tests_device_emu_u2f.sh index 20cf25bd7..d7f7f7296 100755 --- a/core/tests/run_tests_device_emu_u2f.sh +++ b/core/tests/run_tests_device_emu_u2f.sh @@ -25,7 +25,7 @@ if [[ $RUN_TEST_EMU > 0 ]]; then $MICROPYTHON $ARGS "${MAIN}" &> "${TREZOR_LOGFILE}" & upy_pid=$! cd - - sleep 1 + sleep 30 fi # run tests